BFF Pattern — Bảo mật SPA hiện đại với ASP.NET Core và YARP
Posted on: 4/25/2026 7:18:47 AM
Table of contents
- 1. Vấn đề gốc: Token trong Browser là rủi ro lớn
- 2. BFF Pattern: Kiến trúc tổng quan
- 3. Triển khai BFF với ASP.NET Core + YARP
- 4. Vue SPA Integration
- 5. Token Refresh: Transparent cho SPA
- 6. So sánh: SPA truyền thống vs BFF Pattern
- 7. Session Management ở quy mô Production
- 8. Xử lý Multiple Downstream APIs
- 9. Bảo mật nâng cao: DPoP và Sender-Constrained Tokens
- 10. Deployment Patterns
- 11. Monitoring và Observability
- 12. Khi nào KHÔNG nên dùng BFF?
- 13. Checklist triển khai BFF Production
- Kết luận
Bạn đang xây dựng một SPA (Single Page Application) với Vue hoặc React, backend là ASP.NET Core API, xác thực qua OAuth 2.0 / OpenID Connect. Mọi thứ hoạt động: access token lưu trong localStorage, gọi API kèm header Authorization: Bearer .... Rồi một ngày, bạn nhận được báo cáo pentest: "Token lộ qua XSS, session không thể thu hồi, refresh token nằm trong JavaScript". Đó chính xác là vấn đề mà BFF Pattern giải quyết.
BFF — Backend-For-Frontend — không phải là một thư viện hay framework. Đó là một kiến trúc pattern trong đó server-side đứng giữa SPA và Identity Provider (IdP), giữ token hoàn toàn phía server, chỉ giao tiếp với browser qua HttpOnly cookie. Bài viết này sẽ đi sâu vào lý do, kiến trúc, triển khai thực tế với ASP.NET Core + YARP, và những bài học production.
1. Vấn đề gốc: Token trong Browser là rủi ro lớn
Trong mô hình SPA truyền thống, luồng xác thực thường diễn ra như sau: SPA redirect sang IdP → user đăng nhập → IdP trả về authorization code → SPA đổi code lấy access token + refresh token → lưu vào localStorage hoặc sessionStorage → gọi API kèm Bearer token.
sequenceDiagram
participant B as Browser SPA
participant IdP as Identity Provider
participant API as Backend API
B->>IdP: 1. Redirect đăng nhập (PKCE)
IdP-->>B: 2. Authorization Code
B->>IdP: 3. Đổi code → access_token + refresh_token
IdP-->>B: 4. Tokens trả về JavaScript
Note over B: ⚠️ Tokens nằm trong JS memory/localStorage
B->>API: 5. API call + Bearer token
API-->>B: 6. Response
Vấn đề nằm ở bước 4: tokens được trả về cho JavaScript. Điều này tạo ra ba rủi ro chính:
🔓 XSS Token Theft
Một lỗ hổng XSS duy nhất (từ dependency bên thứ ba, input không sanitize, hoặc CDN bị compromise) cho phép attacker đọc localStorage và gửi token về server riêng. Access token + refresh token bị đánh cắp = attacker có full access cho đến khi token hết hạn hoặc bị thu hồi.
🔄 Không thể thu hồi Session
JWT là stateless — một khi đã phát hành, không có cách nào "thu hồi" nó từ phía server trừ khi bạn build thêm blacklist (mất đi ưu điểm stateless). Nếu user đổi mật khẩu hoặc bị lock tài khoản, token cũ vẫn hoạt động cho đến khi expire.
📡 Token Relay qua Network
Mỗi API call đều gửi access token qua network trong header Authorization. Nếu HTTPS bị intercept (corporate proxy, mTLS misconfiguration), token lộ hoàn toàn. Với cookie HttpOnly + Secure, browser tự quản lý và không cho JavaScript truy cập.
⚠️ OAuth 2.0 Security Best Current Practice
RFC 6749 (OAuth 2.0) ban đầu cho phép Implicit Flow cho SPA. Tuy nhiên, OAuth 2.0 Security BCP (draft-ietf-oauth-security-topics) và OAuth 2.1 đã chính thức khuyến nghị: SPA KHÔNG NÊN nhận và lưu trữ token trực tiếp. Thay vào đó, nên sử dụng BFF pattern hoặc Token Mediating Backend. Đây không còn là "nice-to-have" mà là best practice được IETF công nhận.
2. BFF Pattern: Kiến trúc tổng quan
Ý tưởng cốt lõi của BFF rất đơn giản: đặt một server giữa SPA và mọi thứ khác. Server này (BFF) thực hiện toàn bộ luồng OAuth/OIDC, giữ access token + refresh token trong session phía server, và giao tiếp với browser chỉ qua HttpOnly cookie.
graph TB
subgraph Browser
SPA[Vue / React SPA]
end
subgraph BFF Server
COOKIE[Cookie Auth
HttpOnly + Secure + SameSite]
SESSION[Server-side Session
access_token + refresh_token]
PROXY[YARP Reverse Proxy]
CSRF[Anti-CSRF Middleware]
end
subgraph External
IDP[Identity Provider
Entra ID / Keycloak / Auth0]
API1[User API]
API2[Order API]
API3[Product API]
end
SPA -- "Cookie (tự động)" --> COOKIE
COOKIE --> CSRF
CSRF --> SESSION
SESSION -- "Gắn Bearer token" --> PROXY
PROXY --> API1
PROXY --> API2
PROXY --> API3
COOKIE -. "OIDC Flow" .-> IDP
style SPA fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style COOKIE fill:#e94560,stroke:#fff,color:#fff
style SESSION fill:#e94560,stroke:#fff,color:#fff
style PROXY fill:#e94560,stroke:#fff,color:#fff
style CSRF fill:#e94560,stroke:#fff,color:#fff
style IDP fill:#2c3e50,stroke:#fff,color:#fff
style API1 fill:#2c3e50,stroke:#fff,color:#fff
style API2 fill:#2c3e50,stroke:#fff,color:#fff
style API3 fill:#2c3e50,stroke:#fff,color:#fff
Luồng hoạt động chi tiết:
sequenceDiagram
participant B as Browser (SPA)
participant BFF as BFF Server
participant IdP as Identity Provider
participant API as Backend API
B->>BFF: 1. GET /bff/login
BFF->>IdP: 2. OIDC Authorization Request (PKCE)
IdP-->>B: 3. Login page
B->>IdP: 4. User đăng nhập
IdP-->>BFF: 5. Authorization Code callback
BFF->>IdP: 6. Đổi code → tokens (server-to-server)
IdP-->>BFF: 7. access_token + refresh_token
Note over BFF: Tokens lưu trong session, KHÔNG gửi cho browser
BFF-->>B: 8. Set-Cookie: HttpOnly, Secure, SameSite=Lax
B->>BFF: 9. GET /api/products (Cookie tự động gắn)
BFF->>BFF: 10. Validate cookie + CSRF header
BFF->>BFF: 11. Đọc access_token từ session
BFF->>API: 12. GET /products + Authorization: Bearer {token}
API-->>BFF: 13. Response data
BFF-->>B: 14. Response data (không có token)
✅ Điểm mấu chốt
Browser không bao giờ thấy access token hay refresh token. Cookie chỉ chứa session identifier (hoặc encrypted session), không chứa JWT. JavaScript không thể đọc cookie (HttpOnly). Mỗi request SPA gọi BFF, browser tự động gắn cookie — SPA code không cần quản lý token, không cần interceptor, không cần refresh logic.
3. Triển khai BFF với ASP.NET Core + YARP
YARP (Yet Another Reverse Proxy) là thư viện reverse proxy do Microsoft phát triển, tích hợp sâu vào ASP.NET Core pipeline. Dùng YARP làm proxy layer trong BFF cho phép bạn forward request sang downstream API mà vẫn inject token từ server-side session.
3.1. Cấu trúc Project
MyApp.Bff/ ← BFF project (ASP.NET Core)
├── Program.cs ← Cấu hình auth + YARP + middleware
├── appsettings.json ← OIDC config + YARP routes
├── Endpoints/
│ ├── BffLoginEndpoints.cs ← /bff/login, /bff/logout, /bff/user
│ └── BffCsrfMiddleware.cs ← Validate X-CSRF header
└── Transforms/
└── TokenTransform.cs ← Gắn Bearer token vào request
MyApp.Api/ ← Downstream API (validate JWT bình thường)
MyApp.Web/ ← Vue SPA (static files / dev server)
3.2. Program.cs — Cấu hình Authentication
var builder = WebApplication.CreateBuilder(args);
// 1. Authentication: Cookie + OIDC
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("cookie", options =>
{
options.Cookie.Name = "__Host-bff";
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
// Khi cookie expire hoặc invalid → trả 401 thay vì redirect
options.Events.OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = 401;
return Task.CompletedTask;
};
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code"; // Authorization Code Flow
options.ResponseMode = "query"; // Tương thích SameSite cookie
options.UsePkce = true; // PKCE bắt buộc
options.SaveTokens = true; // Lưu tokens vào session
options.MapInboundClaims = false; // Giữ nguyên claim names
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access"); // Để có refresh token
options.Scope.Add("api"); // Scope cho downstream API
options.GetClaimsFromUserInfoEndpoint = true;
});
// 2. YARP Reverse Proxy
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddTransforms(ctx =>
{
ctx.AddRequestTransform(async transformCtx =>
{
var token = await transformCtx.HttpContext
.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(token))
{
transformCtx.ProxyRequest.Headers
.Authorization = new("Bearer", token);
}
});
});
var app = builder.Build();
// 3. Middleware pipeline
app.UseAuthentication();
app.UseAuthorization();
// 4. BFF endpoints
app.MapGet("/bff/login", () => Results.Challenge(
new AuthenticationProperties { RedirectUri = "/" }))
.AllowAnonymous();
app.MapGet("/bff/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync("cookie");
await ctx.SignOutAsync("oidc");
}).RequireAuthorization();
app.MapGet("/bff/user", (HttpContext ctx) =>
{
var user = ctx.User;
if (user.Identity?.IsAuthenticated != true)
return Results.Json(new { isAuthenticated = false });
return Results.Json(new
{
isAuthenticated = true,
claims = user.Claims.Select(c => new { c.Type, c.Value })
});
}).AllowAnonymous();
// 5. YARP proxy — chỉ cho authenticated requests
app.MapReverseProxy().RequireAuthorization();
// 6. SPA fallback
app.MapFallbackToFile("index.html");
app.Run();
3.3. appsettings.json — YARP Routes
{
"Oidc": {
"Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret"
},
"ReverseProxy": {
"Routes": {
"api-route": {
"ClusterId": "api-cluster",
"Match": { "Path": "/api/{**catch-all}" },
"Transforms": [
{ "PathRemovePrefix": "/api" }
]
}
},
"Clusters": {
"api-cluster": {
"Destinations": {
"primary": { "Address": "https://api.myapp.com" }
}
}
}
}
}
3.4. Anti-CSRF Protection
Cookie-based auth dễ bị CSRF (Cross-Site Request Forgery). BFF cần bảo vệ bằng cách yêu cầu mọi request từ SPA phải kèm custom header — browser tự động gắn cookie nhưng JavaScript từ domain khác không thể set custom header do Same-Origin Policy.
// BffCsrfMiddleware.cs
public class BffCsrfMiddleware
{
private readonly RequestDelegate _next;
public BffCsrfMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// Chỉ check cho API proxy routes, bỏ qua /bff/* endpoints
if (context.Request.Path.StartsWithSegments("/api"))
{
if (!context.Request.Headers.ContainsKey("X-CSRF"))
{
context.Response.StatusCode = 403;
await context.Response
.WriteAsync("Missing X-CSRF header");
return;
}
}
await _next(context);
}
}
// Đăng ký trong Program.cs (trước MapReverseProxy):
app.UseMiddleware<BffCsrfMiddleware>();
Tại sao X-CSRF header chứ không dùng AntiForgeryToken?
AntiForgeryToken của ASP.NET Core yêu cầu server render form với token — không phù hợp với SPA. Custom header X-CSRF: 1 (giá trị bất kỳ) đơn giản hơn và equally effective: browser sẽ trigger CORS preflight cho cross-origin request với custom header, và JavaScript từ domain khác không thể gửi request có custom header đến domain của bạn mà không có CORS cho phép.
4. Vue SPA Integration
Phía Vue (hoặc bất kỳ SPA nào), bạn không cần cài thêm bất kỳ thư viện auth nào. Không cần oidc-client-ts, không cần msal-browser. Chỉ cần gọi API qua BFF và để browser tự quản lý cookie.
// composables/useAuth.ts
export function useAuth() {
const user = ref<UserInfo | null>(null)
const isLoading = ref(true)
async function fetchUser() {
try {
const res = await fetch('/bff/user', {
credentials: 'include' // Gửi cookie
})
user.value = await res.json()
} finally {
isLoading.value = false
}
}
function login() {
window.location.href = '/bff/login'
}
function logout() {
window.location.href = '/bff/logout'
}
onMounted(fetchUser)
return { user, isLoading, login, logout }
}
// composables/useApi.ts
export function useApi() {
async function apiFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`/api${path}`, {
...options,
credentials: 'include',
headers: {
...options.headers,
'X-CSRF': '1', // Anti-CSRF header
'Content-Type': 'application/json',
},
})
if (res.status === 401) {
window.location.href = '/bff/login'
throw new Error('Unauthorized')
}
return res
}
return { apiFetch }
}
Đó là tất cả code phía SPA. Không có token management, không interceptor phức tạp, không refresh logic. Browser tự gắn cookie, server tự inject token.
5. Token Refresh: Transparent cho SPA
Khi access token hết hạn, BFF tự động dùng refresh token để lấy token mới — hoàn toàn invisible với SPA.
sequenceDiagram
participant B as Browser (SPA)
participant BFF as BFF Server
participant IdP as Identity Provider
participant API as Backend API
B->>BFF: GET /api/orders (Cookie)
BFF->>BFF: Đọc access_token từ session
Note over BFF: Token đã expire!
BFF->>IdP: POST /token (refresh_token)
IdP-->>BFF: Cấp access_token mới
BFF->>BFF: Cập nhật session
BFF->>API: GET /orders + Bearer {new_token}
API-->>BFF: 200 OK + data
BFF-->>B: 200 OK + data
Note over B: SPA không biết token vừa được refresh
// Xử lý token refresh trong YARP Transform
ctx.AddRequestTransform(async transformCtx =>
{
var httpCtx = transformCtx.HttpContext;
var token = await httpCtx.GetTokenAsync("access_token");
var expiresAt = await httpCtx.GetTokenAsync("expires_at");
// Kiểm tra token sắp hết hạn (buffer 60 giây)
if (DateTimeOffset.TryParse(expiresAt, out var exp)
&& exp <= DateTimeOffset.UtcNow.AddSeconds(60))
{
var refreshToken = await httpCtx.GetTokenAsync("refresh_token");
var newTokens = await RefreshAccessTokenAsync(
httpCtx, refreshToken);
if (newTokens != null)
{
token = newTokens.AccessToken;
// Cập nhật session với tokens mới
var authResult = await httpCtx
.AuthenticateAsync("cookie");
authResult.Properties.UpdateTokenValue(
"access_token", newTokens.AccessToken);
authResult.Properties.UpdateTokenValue(
"refresh_token", newTokens.RefreshToken);
authResult.Properties.UpdateTokenValue(
"expires_at", newTokens.ExpiresAt.ToString("o"));
await httpCtx.SignInAsync("cookie",
authResult.Principal, authResult.Properties);
}
}
if (!string.IsNullOrEmpty(token))
{
transformCtx.ProxyRequest.Headers
.Authorization = new("Bearer", token);
}
});
6. So sánh: SPA truyền thống vs BFF Pattern
| Tiêu chí | SPA truyền thống (Token trong Browser) | BFF Pattern (Token phía Server) |
|---|---|---|
| Token storage | localStorage / sessionStorage / memory | Server-side session (encrypted cookie) |
| XSS impact | Token bị đánh cắp → full account takeover | Cookie HttpOnly → JS không đọc được token |
| CSRF risk | Không (Bearer token không tự gắn) | Có — cần custom header hoặc SameSite cookie |
| Session revocation | Rất khó (JWT stateless) | Dễ dàng (xoá session phía server) |
| Token refresh | SPA code phải xử lý (interceptor phức tạp) | BFF tự xử lý transparent |
| SPA code complexity | Cao (oidc-client-ts, interceptor, token store) | Thấp (chỉ cần fetch với credentials: 'include') |
| Network exposure | Token trong mọi request header | Cookie session ID (không chứa token) |
| Deployment complexity | Thấp (SPA static + API) | Trung bình (thêm BFF server) |
| Latency | Trực tiếp SPA → API | Thêm 1 hop qua BFF (~50-200ms) |
| OAuth 2.1 compliance | Không đạt (token trong browser) | Đạt chuẩn recommended practice |
7. Session Management ở quy mô Production
Với BFF, session trở thành thành phần quan trọng. Mặc định ASP.NET Core lưu session data trong cookie (encrypted), nhưng ở production bạn cần xem xét session store phân tán.
7.1. Lựa chọn Session Store
| Session Store | Ưu điểm | Nhược điểm | Phù hợp khi |
|---|---|---|---|
| Cookie (encrypted) | Đơn giản, stateless, không cần infra | Giới hạn 4KB, cookie lớn mỗi request | Ít claims, team nhỏ |
| SQL Server | Persistent, audit-friendly | Latency cao hơn, cần cleanup job | Đã có SQL Server infra |
| Redis | Nhanh, TTL tự động, scale tốt | Thêm dependency, cost | High-traffic, multi-instance |
| Data Protection API | Tích hợp sẵn ASP.NET Core | Cần share key ring giữa instances | Mặc định cho cookie encryption |
7.2. Cấu hình Data Protection cho multi-instance
// Khi chạy BFF trên nhiều instances (k8s, Azure Container Apps)
// cần share Data Protection key ring
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(
connectionString, "data-protection", "keys.xml")
.ProtectKeysWithAzureKeyVault(
new Uri("https://mykeyvault.vault.azure.net/keys/dp-key"),
new DefaultAzureCredential());
// Hoặc với Redis:
builder.Services.AddDataProtection()
.PersistKeysToStackExchangeRedis(
ConnectionMultiplexer.Connect("redis:6379"),
"DataProtection-Keys");
8. Xử lý Multiple Downstream APIs
Trong microservices, BFF thường proxy request đến nhiều API khác nhau. YARP hỗ trợ điều này qua multiple routes và clusters.
graph LR
SPA[Vue SPA] --> BFF
subgraph BFF[BFF Server - YARP]
R1[/api/users/**]
R2[/api/orders/**]
R3[/api/products/**]
R4[/api/notifications/**]
end
R1 --> US[User Service
:5001]
R2 --> OS[Order Service
:5002]
R3 --> PS[Product Service
:5003]
R4 --> NS[Notification Service
:5004]
style SPA fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style BFF fill:#e94560,stroke:#fff,color:#fff
style R1 fill:#e94560,stroke:#fff,color:#fff
style R2 fill:#e94560,stroke:#fff,color:#fff
style R3 fill:#e94560,stroke:#fff,color:#fff
style R4 fill:#e94560,stroke:#fff,color:#fff
style US fill:#2c3e50,stroke:#fff,color:#fff
style OS fill:#2c3e50,stroke:#fff,color:#fff
style PS fill:#2c3e50,stroke:#fff,color:#fff
style NS fill:#2c3e50,stroke:#fff,color:#fff
{
"ReverseProxy": {
"Routes": {
"users": {
"ClusterId": "user-service",
"Match": { "Path": "/api/users/{**rest}" },
"Transforms": [
{ "PathPattern": "/users/{**rest}" }
]
},
"orders": {
"ClusterId": "order-service",
"Match": { "Path": "/api/orders/{**rest}" },
"Transforms": [
{ "PathPattern": "/orders/{**rest}" }
]
},
"products": {
"ClusterId": "product-service",
"Match": { "Path": "/api/products/{**rest}" },
"Transforms": [
{ "PathPattern": "/products/{**rest}" }
]
}
},
"Clusters": {
"user-service": {
"Destinations": {
"primary": { "Address": "https://users.internal:5001" }
}
},
"order-service": {
"Destinations": {
"primary": { "Address": "https://orders.internal:5002" }
}
},
"product-service": {
"Destinations": {
"primary": { "Address": "https://products.internal:5003" }
}
}
}
}
}
9. Bảo mật nâng cao: DPoP và Sender-Constrained Tokens
BFF pattern giải quyết vấn đề token trong browser, nhưng giữa BFF và downstream API, access token vẫn là Bearer token truyền thống — nếu bị intercept ở đoạn này, attacker vẫn có thể replay. DPoP (Demonstrating Proof-of-Possession) giải quyết vấn đề này.
sequenceDiagram
participant BFF as BFF Server
participant IdP as Identity Provider
participant API as Backend API
BFF->>BFF: 1. Tạo DPoP key pair (asymmetric)
BFF->>IdP: 2. Token request + DPoP proof JWT
IdP-->>BFF: 3. DPoP-bound access_token
Note over BFF: Token chỉ hợp lệ khi kèm DPoP proof
từ cùng key pair
BFF->>API: 4. Request + DPoP token + DPoP proof
API->>API: 5. Verify: proof.key == token.cnf.jkt
API-->>BFF: 6. 200 OK
Note over API: Nếu attacker đánh cắp token
nhưng không có private key → REJECT
✅ Khi nào cần DPoP?
DPoP thêm complexity (key management, proof generation mỗi request). Nếu BFF → API communication hoàn toàn trong private network (Kubernetes internal, VPC), rủi ro token intercept rất thấp và DPoP có thể không cần thiết. DPoP quan trọng hơn khi: BFF → API qua public internet, hoặc yêu cầu compliance cao (PCI-DSS, financial services).
10. Deployment Patterns
10.1. Cùng Origin (Recommended)
BFF và SPA cùng origin (cùng domain + port) là setup đơn giản và an toàn nhất — không cần CORS, cookie SameSite hoạt động tự nhiên.
# Docker Compose
services:
bff:
build: ./MyApp.Bff
ports:
- "443:8080"
environment:
- ASPNETCORE_URLS=http://+:8080
volumes:
- ./MyApp.Web/dist:/app/wwwroot # SPA static files
# BFF phục vụ cả SPA files (MapFallbackToFile)
# và proxy API requests (YARP)
# → Cùng origin, zero CORS issues
10.2. Khác Origin (Khi cần tách deployment)
// Nếu SPA ở CDN (spa.myapp.com) và BFF ở api.myapp.com
// → Cần CORS + credentials
builder.Services.AddCors(options =>
{
options.AddPolicy("spa", policy =>
{
policy.WithOrigins("https://spa.myapp.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Bắt buộc cho cookie
});
});
// Cookie config phải thay đổi:
options.Cookie.SameSite = SameSiteMode.None; // Cross-site
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.Domain = ".myapp.com"; // Shared parent domain
⚠️ Cross-origin BFF giảm bảo mật
SameSite=None cho phép cookie gửi cross-site, mở rộng attack surface cho CSRF. Nếu có thể, hãy giữ BFF và SPA cùng origin bằng cách deploy SPA static files trong BFF hoặc dùng reverse proxy phía trước (Nginx, Cloudflare) để route cả hai về cùng domain.
11. Monitoring và Observability
BFF là điểm trung tâm xử lý toàn bộ traffic từ SPA, vì vậy đây là vị trí lý tưởng để thu thập metrics và traces.
📊 Metrics cần theo dõi
Authentication: Login success/failure rate, token refresh frequency, session count active. Proxy: Request latency (P50/P95/P99), error rate per downstream, CSRF rejection count. Security: 401/403 rate, suspicious pattern detection.
📡 Distributed Tracing
BFF nên propagate trace context (W3C TraceContext) sang downstream APIs. Mỗi request từ SPA → BFF → API tạo thành một trace hoàn chỉnh, giúp debug latency issues. ASP.NET Core + YARP tự động propagate khi cấu hình OpenTelemetry.
🔍 Audit Logging
Mỗi request qua BFF đều có user context (từ cookie session). Log request path + user ID + timestamp cho audit trail. Đây là lợi thế lớn so với API Gateway thuần (không có user context ở level request routing).
12. Khi nào KHÔNG nên dùng BFF?
BFF pattern không phải silver bullet. Có những tình huống mà nó không phù hợp hoặc không cần thiết:
- Public API mà third-party cần gọi trực tiếp: BFF phục vụ SPA cụ thể, không phù hợp cho external consumers. External clients nên dùng OAuth2 client credentials flow trực tiếp.
- Mobile native apps: iOS/Android có secure storage (Keychain, Keystore) để lưu token an toàn hơn browser. BFF vẫn có thể dùng nhưng overhead cao hơn lợi ích.
- Server-rendered apps (MVC, Razor Pages, Blazor Server): Đã có server-side session natively. Thêm BFF layer là redundant.
- Internal tools với risk thấp: Admin dashboard nội bộ sau VPN, không chứa dữ liệu nhạy cảm — token trong memory có thể chấp nhận được.
13. Checklist triển khai BFF Production
Kết luận
BFF pattern không phải là phát minh mới — nhưng với sự thay đổi trong landscape bảo mật (OAuth 2.1, XSS ngày càng tinh vi, compliance requirements ngày càng chặt), nó đã trở thành recommended architecture cho bất kỳ SPA nào cần xử lý authentication nghiêm túc. YARP trên ASP.NET Core làm cho việc triển khai BFF trở nên dễ dàng: một project duy nhất vừa handle OIDC, vừa proxy API, vừa phục vụ SPA static files.
Lợi ích lớn nhất không phải chỉ là bảo mật — mà là đơn giản hoá code SPA. Khi token management chuyển hoàn toàn sang server, SPA code trở nên sạch sẽ hơn, dễ test hơn, và team frontend không cần hiểu sâu về OAuth flows. Đó là win-win cho cả security và developer experience.
✅ Tóm tắt
BFF Pattern = Server giữ token, browser chỉ có cookie. YARP + ASP.NET Core = proxy + auth trong 1 project. Cookie HttpOnly + SameSite + X-CSRF = defense-in-depth. Token refresh transparent = SPA code đơn giản. OAuth 2.1 khuyến nghị pattern này cho mọi SPA.
Nguồn tham khảo:
- OAuth 2.0 for Browser-Based Applications (IETF Draft)
- Duende BFF Documentation
- YARP — Yet Another Reverse Proxy by Microsoft
- BFF in ASP.NET Core — Implementing from scratch (Tore Nestenius)
- RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- BFF Pattern Guide with .NET and Vue (Tural Hasanov)
GraphQL Federation — Xây dựng API Gateway thống nhất cho Microservices
Distributed Locking — Giải quyết Race Condition trong hệ thống phân tán với Redis và .NET 10
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.