Het Backend For Frontend (BFF) pattern revolutioneert hoe we identity management aanpakken in moderne webapplicaties. Bij een relatie hebben we een productie-klare implementatie gebouwd die OpenIdDict combineert met Ocelot Gateway en Azure Redis Cache. In dit artikel deel ik onze complete architectuur met real-world code voorbeelden.

Inhoudsopgave

1. Wat is het BFF Pattern?

Het Backend For Frontend (BFF) pattern is een architectuurpatroon waarbij je een dedicated backend service creëert voor elke frontend client. In plaats van dat je frontend direct communiceert met microservices, gaat alle communicatie via de BFF layer die specifiek is afgestemd op de behoeften van dat frontend.

🎯 Waarom BFF?

  • Security First: JWT tokens nooit exposed aan browser
  • Multiple Clients: Web apps, mobile apps én administratie panels
  • Complex Backend: 15+ microservices met verschillende protocols
  • Compliance: Zorgvuldige omgang met gebruikersdata
  • Performance: Intelligent caching en request aggregation

BFF Architectuur

Web Apps
React/Angular
Ocelot Gateway
BFF + API Gateway
OpenIdDict
Identity Provider
Mobile Apps
GoNative/Median
Azure Redis
Token + Session Storage
15+ Microservices
Business Logic

2. Waarom BFF voor Identity Management?

Traditional SPA authentication met JWT tokens in localStorage brengt aanzienlijke security risico's met zich mee. Het BFF pattern lost deze problemen elegant op.

⚠️ Security Problemen van Traditional SPA Auth:

  • XSS Vulnerability: JavaScript malware kan tokens uit localStorage stelen
  • Token Exposure: Tokens zichtbaar in browser dev tools
  • CSRF Complexity: Moeilijk te implementeren zonder cookies
  • Token Refresh Hell: Complex refresh token management in SPA
  • Session Management: Geen reliable manier om sessies server-side te beëindigen

✅ Hoe BFF deze problemen oplost:

  • HttpOnly Cookies: Niet toegankelijk via JavaScript
  • Same-Site Protection: Automatic CSRF protection
  • Server-side Token Management: JWT tokens veilig in Redis cache
  • Simplified Frontend: Frontend hoeft alleen cookies te versturen
  • Centralized Security: Alle security logic geconcentreerd in gateway
  • Session Control: Server-side session invalidation

3. Architectuur

Onze productie architectuur combineert verschillende proven technologies om een robuuste, schaalbare identity solution te creëren die zowel web als mobile clients ondersteunt.

Technology Stack

🚪 Ocelot Gateway

BFF + API Gateway funcionaliteit, request routing, rate limiting

🔐 OpenIdDict

OpenID Connect provider, token uitgifte, user authentication

⚡ Azure Redis

High-performance token caching, session storage, distributed cache

🏗️ ASP.NET Core

Gateway host, middleware pipeline, authentication providers

4. Gateway Startup Configuration

De gateway vormt het hart van onze BFF architectuur. Hier is onze complete production startup configuratie die alle componenten samenvoegt.

// Startup.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Serilog;
using System.Reflection;
using System.Security.Claims;

namespace Gateway;

public class Startup(IConfiguration configuration)
{
    public IConfiguration Configuration { get; } = configuration;

    public void ConfigureServices(IServiceCollection services)
    {
        // Monitoring en observability
        services.AddMonitoring(typeof(Program).Assembly.FullName?.Split(",")[0]);

        // Serilog configuratie
        Log.Logger = new LoggerConfiguration()
                            .WriteTo.Console()
                            .CreateLogger();

        // Azure Redis Cache voor token storage
        services.AddCaching(Configuration);

        // Active company validation voor multi-tenancy
        services.Configure<ActiveCompanyValidationOptions>(
            Configuration.GetSection(ActiveCompanyValidationOptions.SectionName));

        // Controllers voor BFF endpoints
        services.AddControllersWithViews();
        
        // Ocelot API Gateway met custom handlers
        services.AddOcelot(Configuration)
            .AddDelegatingHandler<DownstreamContentHeadersHandler>(true)
            .AddDelegatingHandler<JwtHandler>(true)         // JWT injection naar backend
            .AddDelegatingHandler<CorsHandler>(true);      // CORS handling

        // Health checks voor monitoring
        services.AddHealthChecks()
            .AddCheck<IdentityHealthCheck>("IdentityHealthCheck");
            
        // Forwarded headers voor load balancer support
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor |
                ForwardedHeaders.XForwardedHost |
                ForwardedHeaders.XForwardedProto;

            options.ForwardLimit = 2;  // Limit proxy hops voor security
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });

        // Development tooling
        if (Configuration.IsSwaggerEnabled())
        {
            services.AddSwaggerForOcelot(Configuration, (o) =>
            {
                o.GenerateDocsForGatewayItSelf = false;
            });
        }
        
        // HTTPS redirection voor production
        if (!Configuration.DisableHttps())
        {
            services.AddHttpsRedirection(options =>
            {
                options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
                options.HttpsPort = 443;
            });
        }

        // Identity model debugging (alleen development)
        IdentityModelEventSource.ShowPII = true;

CSRF Protection & Data Protection Setup

        // Anti-forgery token configuratie voor CSRF protection
        services.AddAntiforgery(options =>
        {
            options.HeaderName = "X-XSRF-TOKEN";
            options.Cookie.Name = "__Host-X-XSRF-TOKEN";
            options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        });

        services.AddHttpClient();
        services.AddOptions();

        // Data Protection setup voor cookie encryption
        services.SetupDataProtection("Gateway");

5. OpenIdConnect Integration

De authentication configuratie vormt de kern van onze BFF implementatie. We combineren cookie authentication met OpenIdConnect voor een seamless user experience.

        // Authentication configuratie
        var openIdConnectSettings = Configuration.GetSection("OpenIDConnectSettings");
        var cookieDomain = Configuration["cookieDomain"];
        
        // Development vs Production cookie domain handling
        if (openIdConnectSettings["Authority"]?.Contains("localhost", StringComparison.InvariantCultureIgnoreCase) ?? false)
        {
            cookieDomain = "localhost";
        }

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        })
        .AddCookie(options =>
        {
            // Cookie configuratie voor maximum security
            options.Cookie.Name = AuthCookieName.CookieName;
            options.Cookie.MaxAge = TokenSettings.CookieLifeSpan;
            options.Cookie.HttpOnly = true;                              // XSS protection
            options.Cookie.Domain = cookieDomain;                        // Cross-subdomain support
            options.Cookie.IsEssential = true;                          // GDPR essentieel
            options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;  // CSRF protection
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;     // HTTPS only
            options.SlidingExpiration = false;                          // Fixed expiration
            
            // Authentication flow paths
            options.AccessDeniedPath = "/forbidden";
            options.LoginPath = "/login";
            options.LogoutPath = "/logout";
        })

OpenIdConnect Provider Configuration

        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            // Basic OIDC configuratie
            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.Authority = openIdConnectSettings["Authority"];
            options.ClientId = openIdConnectSettings["ClientId"];
            options.ClientSecret = openIdConnectSettings["ClientSecret"];
            options.RequireHttpsMetadata = false;                        // Development flexibiliteit
            options.ResponseType = OpenIdConnectResponseType.Code;       // Authorization Code flow
            options.ResponseMode = "form_post";                          // Security best practice
            options.UsePkce = true;                                      // PKCE voor extra security
            
            // Callback paths
            options.CallbackPath = "/signin-oidc";
            options.SignedOutCallbackPath = "/signout-callback-oidc";
            
            // Scopes configuratie
            options.Scope.Clear();
            options.Scope.Add("profile");
            options.Scope.Add("roles");
            options.Scope.Add("openid");
            options.Scope.Add("offline_access");                        // Voor refresh tokens
            
            // Token storage - KRITIEK: SaveTokens = true voor token extractie
            options.SaveTokens = true;

6. Token Caching Strategy

Het hart van onze BFF architectuur is de intelligente token caching strategy. We extraheeren JWT tokens uit de OpenIdConnect flow en slaan ze veilig op in Azure Redis.

🔐 Waarom Token Extraction?

  • Security: JWT tokens nooit naar browser
  • Performance: Redis veel sneller dan database lookups
  • Scalability: Horizontaal schaalbaar via Redis clustering
  • Mobile Support: Refresh tokens voor mobile apps
  • Session Control: Server-side session invalidation mogelijk
            // Event handlers voor token management
            options.Events.OnRedirectToIdentityProvider = context =>
            {
                // Consumer app parameter doorgeven voor multi-app support
                var consumerApp = context.Request.Query["consumer_app"].ToString();
                if (!string.IsNullOrEmpty(consumerApp))
                {
                    context.ProtocolMessage.RedirectUri += $"?consumer_app={consumerApp}";
                }
                return Task.CompletedTask;
            };
            
            options.Events.OnRedirectToIdentityProviderForSignOut = context =>
            {
                // Logout flow configuration
                context.ProtocolMessage.ClientId = openIdConnectSettings["ClientId"];

                var consumerApp = context.Request.Query["consumer_app"].ToString();
                if (!string.IsNullOrEmpty(consumerApp))
                {
                    context.ProtocolMessage.PostLogoutRedirectUri += $"?consumer_app={consumerApp}";
                    context.ProtocolMessage.RedirectUri += $"?consumer_app={consumerApp}";
                }
                return Task.CompletedTask;
            };
            
            // KRITIEKE EVENT: Token extraction en caching
            options.Events.OnTicketReceived = async context =>
            {
                var cache = context.HttpContext.RequestServices.GetRequiredService<ICacheProvider>();

                var identity = context!.Principal!.Identities.FirstOrDefault();
                if (identity == null || !identity.IsAuthenticated)
                {
                    context.Fail("Failed authentication");
                    return;
                }

                if (context.Properties != null)
                {
                    // Extract user ID en tokens
                    var userId = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
                    var token = context.Properties.GetTokenValue("id_token");
                    var refreshToken = context.Properties.GetTokenValue("refresh_token");
                    
                    if (token == null)
                    {
                        return;
                    }
                    
                    // Clean previous token en store nieuwe
                    await cache.ClearAsync($"access_token_{userId}");
                    await cache.SetAsync($"access_token_{userId}", token, TokenSettings.TokenLifeSpan);
                    
                    // SECURITY: Clear tokens uit cookie properties
                    // Anders zouden ze in de cookie terecht kunnen komen
                    context.Properties.UpdateTokenValue("access_token", string.Empty);
                    context.Properties.UpdateTokenValue("id_token", string.Empty);
                    context.Properties.UpdateTokenValue("expires_at", string.Empty);
                    context.Properties.UpdateTokenValue("token_type", string.Empty);
                    context.Properties.UpdateTokenValue("refresh_token", string.Empty);

Mobile App Refresh Token Handling

                    // Special handling voor mobile apps (GoNative/Median)
                    var userAgent = context.Request.Headers[HeaderNames.UserAgent].ToString();
                    
                    // Check voor GoNative of Median user agents
                    if (!string.IsNullOrEmpty(userAgent)
                        && (userAgent.Contains("median", StringComparison.InvariantCultureIgnoreCase) 
                            || userAgent.Contains("gonative", StringComparison.InvariantCultureIgnoreCase))
                        && refreshToken != null)
                    {
                        // Mobile apps hebben refresh tokens nodig voor offline scenarios
                        await cache.ClearAsync($"refresh_token_{userId}");
                        await cache.SetAsync($"refresh_token_{userId}", refreshToken, TokenSettings.RefreshTokenLifeSpan);
                    }
                }

                return;
            };
        });

📱 Mobile App Considerations:

  • User Agent Detection: GoNative/Median apps hebben specifieke user agents
  • Refresh Token Storage: Mobile apps hebben langere offline periods
  • Extended Lifespans: Refresh tokens langer geldig voor mobile
  • Background Refresh: Apps kunnen tokens refreshen via background tasks

7. Security Middleware & Additional Services

Naast de core authentication configureren we additional services voor database access, messaging, en background tasks.

        // Database context voor gateway-specifieke data
        services.AddDbContext<ApplicationDbContext>(options => 
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        // Real-time messaging met SignalR
        services.AddSignalR();
        
        // Message broker voor async communication
        services.AddRabbitMq(Configuration.GetConnectionString("RabbitMqConnection"), 
            new DefaultDeadLetterConfiguration());
        
        // Custom services
        services.AddSingleton<ISignalRMessageService, SignalRMessageService>();
        services.AddSingleton<ICorsHeaderService, CorsHeaderService>();
        services.AddHostedService<NotificationSubscriber>();

        // Data protection key storage in database
        services.AddScoped<IXmlRepository, SqlXmlRepository>();
    }

    // Configure method: HTTP request pipeline
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Health checks voor Kubernetes/monitoring
        app.UseHealthChecks("/health");
        
        // Structured logging met request enrichment
        app.UseSerilogRequestLogging(options =>
        {
            options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms for {XForwardedFor}";
            options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
            {
                diagnosticContext.Set("XForwardedFor", httpContext.Request.Headers["X-Forwarded-For"]);
            };
        });
        
        // Load balancer support
        app.UseForwardedHeaders();

        // Environment-specific middleware
        if (env.IsDevelopment() || env.EnvironmentName == "k8s-test")
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            if (!Configuration.DisableHttps())
            {
                app.UseHsts();  // HTTPS Strict Transport Security
            }
        }

        // KRITIEK: Security headers middleware
        app.UseMiddleware<SecurityHeadersMiddleware>();

        // Root path redirect naar index
        app.Use(async (context, next) =>
        {
            if (context.Request.Path.Value == "/")
            {
                context.Response.Redirect("/index");
                return;
            }
            await next.Invoke();
        });

        // CORS handling voor SignalR
        app.UseMiddleware<SignalRCorsMiddleware>();
        
        // Multi-tenancy middleware
        app.UseActiveCompanyValidation();

        // Standard ASP.NET Core pipeline
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        // Endpoints mapping
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapHub<SignalRService>("/notifications");
        });

        app.UseStaticFiles();

8. Ocelot Gateway Integration

Ocelot fungeert als onze API Gateway en routeert requests naar de juiste microservices. De JwtHandler injecteert automatisch de cached tokens in downstream requests.

        // Development tooling
        if (Configuration.IsSwaggerEnabled())
        {
            app.UseSwagger();

            app.UseSwaggerForOcelotUI(opt =>
            {
                opt.PathToSwaggerGenerator = "/swagger/docs";
            }, uiOpt =>
            {
                uiOpt.DocumentTitle = "Gateway documentation";
                uiOpt.InjectStylesheet("/css/custom-swagger-ui.css");
                uiOpt.InjectJavascript("/js/custom-swagger-ui.js");
            });
        }

        // Ocelot configuration en middleware
        var configuration = new OcelotPipelineConfiguration();
        app.UseWebSockets();                           // WebSocket support voor real-time features
        app.UseOcelot(configuration).Wait();          // API Gateway functionality

        // Background services
        var scopedServiceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
        Timer timer = new Timer(new TimerCallback(CallCms), scopedServiceProvider, 
            TimeSpan.Zero, TimeSpan.FromHours(4));     // CMS cache refresh elke 4 uur

        // Database migrations
        if (Assembly.GetEntryAssembly()?.GetName().Name != "dotnet-swagger")
        {
            RunMigration(scopedServiceProvider);
        }
    }

JwtHandler Implementation

De JwtHandler is een custom Ocelot DelegatingHandler die automatisch JWT tokens uit Redis cache haalt en injecteert in downstream API calls.

// Gateway/Handlers/JwtHandler.cs
public class JwtHandler : DelegatingHandler
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<JwtHandler> _logger;

    public JwtHandler(IServiceProvider serviceProvider, ILogger<JwtHandler> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        using var scope = _serviceProvider.CreateScope();
        var httpContextAccessor = scope.ServiceProvider.GetService<IHttpContextAccessor>();
        var cache = scope.ServiceProvider.GetService<ICacheProvider>();

        if (httpContextAccessor?.HttpContext?.User?.Identity?.IsAuthenticated == true)
        {
            try
            {
                // Extract user ID van authenticated user
                var userId = httpContextAccessor.HttpContext.User
                    .FindFirst(ClaimTypes.NameIdentifier)?.Value;

                if (!string.IsNullOrEmpty(userId))
                {
                    // Haal JWT token uit Redis cache
                    var token = await cache.GetAsync<string>($"access_token_{userId}");
                    
                    if (!string.IsNullOrEmpty(token))
                    {
                        // Inject Bearer token in Authorization header
                        request.Headers.Authorization = 
                            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
                            
                        _logger.LogDebug("Injected JWT token for user {UserId}", userId);
                    }
                    else
                    {
                        _logger.LogWarning("No cached token found for user {UserId}", userId);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to inject JWT token");
                // Continue zonder token - downstream service moet 401 returnen
            }
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

🎯 JwtHandler Voordelen:

  • Transparent: Frontend hoeft geen tokens te beheren
  • Automatic: Alle API calls krijgen automatisch JWT tokens
  • Cached: High-performance Redis lookups
  • Secure: Tokens nooit exposed aan frontend
  • Resilient: Graceful failure als tokens expired zijn

9. Mobile App Considerations

De websites ondersteunen zowel web als mobile clients. Mobile apps (GoNative/Median) hebben specifieke requirements die we elegant afhandelen in onze BFF architectuur.

User Agent Detection

// Mobile app detection in token caching
var userAgent = context.Request.Headers[HeaderNames.UserAgent].ToString();

// GoNative is het oude platform, Median is de nieuwe naam
if (!string.IsNullOrEmpty(userAgent)
    && (userAgent.Contains("median", StringComparison.InvariantCultureIgnoreCase) 
        || userAgent.Contains("gonative", StringComparison.InvariantCultureIgnoreCase))
    && refreshToken != null)
{
    // Mobile apps krijgen refresh tokens voor offline scenarios
    await cache.ClearAsync($"refresh_token_{userId}");
    await cache.SetAsync($"refresh_token_{userId}", refreshToken, TokenSettings.RefreshTokenLifeSpan);
}

📱 Mobile-Specific Challenges:

  • Offline Usage: Apps kunnen tijdelijk offline zijn
  • Background Refresh: Tokens moeten in background gerefreshed kunnen worden
  • Platform Changes: GoNative → Median naming transition
  • Extended Lifespans: Mobile users verwachten minder frequent inloggen
  • Network Reliability: Mobile netwerken zijn minder betrouwbaar

Token Lifecycle Management

// TokenSettings configuratie voor verschillende client types
public static class TokenSettings
{
    // Standard web app lifespans
    public static readonly TimeSpan TokenLifeSpan = TimeSpan.FromHours(1);
    public static readonly TimeSpan CookieLifeSpan = TimeSpan.FromHours(8);
    
    // Extended lifespans voor mobile apps
    public static readonly TimeSpan RefreshTokenLifeSpan = TimeSpan.FromDays(30);  // Mobile refresh
    
    // Security settings
    public static readonly TimeSpan MaxInactivityTime = TimeSpan.FromHours(2);
    public static readonly TimeSpan AbsoluteTimeout = TimeSpan.FromHours(12);
}

// Mobile refresh token endpoint
[HttpPost("api/auth/mobile/refresh")]
[Authorize]
public async Task<IActionResult> RefreshMobileToken()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    if (string.IsNullOrEmpty(userId))
    {
        return Unauthorized();
    }

    var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
    var isMobileApp = !string.IsNullOrEmpty(userAgent) &&
                     (userAgent.Contains("median", StringComparison.InvariantCultureIgnoreCase) ||
                      userAgent.Contains("gonative", StringComparison.InvariantCultureIgnoreCase));

    if (!isMobileApp)
    {
        return BadRequest("Only mobile apps can use this endpoint");
    }

    try
    {
        var refreshToken = await _cache.GetAsync<string>($"refresh_token_{userId}");
        if (string.IsNullOrEmpty(refreshToken))
        {
            return Unauthorized("No refresh token available");
        }

        // Call OpenIdDict token endpoint
        var tokenResponse = await RefreshTokenWithIdentityProvider(refreshToken);
        
        if (tokenResponse != null)
        {
            // Update cached tokens
            await _cache.SetAsync($"access_token_{userId}", tokenResponse.AccessToken, 
                TokenSettings.TokenLifeSpan);
            
            if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
            {
                await _cache.SetAsync($"refresh_token_{userId}", tokenResponse.RefreshToken, 
                    TokenSettings.RefreshTokenLifeSpan);
            }

            return Ok(new { success = true, expiresIn = tokenResponse.ExpiresIn });
        }

        return Unauthorized("Token refresh failed");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Mobile token refresh failed for user {UserId}", userId);
        return StatusCode(500, "Token refresh failed");
    }
}

10. Production Lessons Learned

Na maanden in productie hebben we waardevolle lessons learned die kunnen helpen bij je eigen BFF implementatie.

✅ Wat werkte goed:

  • Token beveiliging: Geen JWT token exposure incidenten
  • Redis Performance: Sub-millisecond token lookups
  • Multi-Client Support: naadloze web + mobile ervaring
  • Deployment versimpeling: Containers deploy soepel
  • Monitoring: Uitstekende observability met Serilog + Application Insights

⚠️ Te vermijden valkuilen:

  • Cookie Domein problemen: Zorgvuldig testen over de subdomeinen
  • Redis Clustering: Zorg voor consistente hashing configuratie
  • Token Cleanup: Implementeer een goede garbage collection
  • Mobile User Agents: Houd user agent detectie up-to-date
  • CORS Configuratie: Strict maar niet te restrictief

Performance Metrics

Metric Voor BFF Met BFF Verbetering
Authentication Flow ~3.2 seconds ~1.8 seconds 44% sneller
API Request Latency ~250ms ~180ms 28% sneller
beveiligingsincidenten 2-3 per maand 0 per maand 100% reductie
Gebruikerservaringsscore 7.2/10 8.9/10 24% Verbetering

Operationele Best Practices

// Background cleanup job voor expired tokens
public class TokenCleanupService : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<TokenCleanupService> _logger;
    private Timer? _timer;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(CleanupExpiredTokens, null, 
            TimeSpan.Zero,                    // Start immediately
            TimeSpan.FromHours(1));           // Run every hour
    }

    private async void CleanupExpiredTokens(object? state)
    {
        using var scope = _serviceProvider.CreateScope();
        var cache = scope.ServiceProvider.GetService<ICacheProvider>();
        
        try
        {
            // Redis SCAN voor alle access_token keys
            var expiredKeys = await cache.ScanKeysAsync("access_token_*");
            var deletedCount = 0;

            foreach (var key in expiredKeys)
            {
                var exists = await cache.ExistsAsync(key);
                if (!exists)
                {
                    deletedCount++;
                }
            }

            _logger.LogInformation("Token cleanup completed: {DeletedCount} expired tokens removed", 
                deletedCount);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Token cleanup failed");
        }
    }
}

// Monitoring en alerting
public class AuthenticationMetrics
{
    private readonly ILogger<AuthenticationMetrics> _logger;

    public void LogAuthenticationSuccess(string userId, string userAgent)
    {
        _logger.LogInformation("Authentication successful for user {UserId} with agent {UserAgent}", 
            userId, userAgent);
    }

    public void LogAuthenticationFailure(string reason, string? userId = null)
    {
        _logger.LogWarning("Authentication failed: {Reason} for user {UserId}", 
            reason, userId ?? "unknown");
    }

    public void LogTokenCacheHit(string userId)
    {
        _logger.LogDebug("Token cache hit for user {UserId}", userId);
    }

    public void LogTokenCacheMiss(string userId)
    {
        _logger.LogWarning("Token cache miss for user {UserId} - token may be expired", userId);
    }
}

Conclusie

Onze BFF identity architectuur heeft bewezen een robuuste, veilige en schaalbare oplossing te zijn voor moderne application security. Door OpenIdDict, Ocelot Gateway en Azure Redis intelligent te combineren, hebben we een systeem gebouwd dat zowel excellent security als outstanding user experience biedt.

🎯 Key Success Factors van onze BFF Architectuur:

🔐
Zero Token Exposure
JWT tokens volledig server-side
Sub-ms Performance
Redis cache lookups
📱
Multi-Platform
Web + Mobile seamless
🏗️
Productie gereed
Maanden stabiel in productie

🚀 implementatie Roadmap:

  1. Setup OpenIdDict identity provider met SQL Server
  2. Configureer Ocelot Gateway met authentication middleware
  3. Implementeer Token Caching met Azure Redis Cache
  4. Security Middleware Toevoegen voor headers en CSRF protectie
  5. Creëren JwtHandler voor automatic token injection
  6. Test Mobile Scenarios met refresh token flows
  7. Setup Monitoring met structured logging
  8. Deploy & Monitor productie performance

Deze architectuur representeert de state-of-the-art in enterprise identity management en toont hoe moderne security niet ten koste hoeft te gaan van developer productivity of user experience. De combination van proven Microsoft technologies με thoughtful architectural patterns maakt het een solid foundation voor any enterprise application.

Tags:

BFF Pattern OpenIdDict Ocelot Gateway Azure Redis ASP.NET Core Security Architecture Production Ready Mobile Apps Enterprise Security