[{"data":1,"prerenderedAt":900},["ShallowReactive",2],{"post-fr-pattern-bff":3},{"id":4,"title":5,"body":6,"date":885,"description":16,"excerpt":886,"extension":887,"meta":888,"navigation":149,"path":889,"readTime":159,"seo":890,"slug":891,"stem":892,"tags":893,"__hash__":899},"fr_blog/fr/blog/pattern-bff.md","BFF Pattern avec FastAPI : mettre un backend devant ton frontend",{"type":7,"value":8,"toc":874},"minimark",[9,13,17,22,38,59,73,77,87,90,94,99,356,360,535,539,654,658,661,784,788,867,870],[10,11,5],"h1",{"id":12},"bff-pattern-avec-fastapi-mettre-un-backend-devant-ton-frontend",[14,15,16],"p",{},"Le pattern Backend-for-Frontend (BFF) n'est pas nouveau — Netflix, SoundCloud et d'autres l'ont popularisé il y a plus d'une décennie. Pourtant, il reste sous-utilisé dans les architectures Vue.js + FastAPI, où la tendance est de gérer les tokens OAuth2 directement côté client. Voici pourquoi ce choix est risqué, et comment le BFF le résout.",[18,19,21],"h2",{"id":20},"le-problème-avec-les-tokens-côté-client","Le problème avec les tokens côté client",[14,23,24,25,29,30,33,34,37],{},"Dans une SPA Vue.js classique avec Azure B2C, le flux OAuth2 aboutit à un ",[26,27,28],"code",{},"access_token"," stocké quelque part côté navigateur : ",[26,31,32],{},"localStorage",", ",[26,35,36],{},"sessionStorage",", ou un cookie. Chacune de ces options a ses limites :",[39,40,41,48,53],"ul",{},[42,43,44,47],"li",{},[45,46,32],"strong",{}," — accessible par tout JavaScript sur le domaine, vulnérable aux attaques XSS",[42,49,50,52],{},[45,51,36],{}," — mêmes problèmes, disparaît à la fermeture de l'onglet",[42,54,55,58],{},[45,56,57],{},"Cookie HttpOnly"," — meilleure option côté client, mais le refresh token doit toujours être géré",[14,60,61,62,65,66,69,70,72],{},"Le vrai problème : le ",[26,63,64],{},"client_secret"," Azure B2C ne peut pas être embarqué dans une SPA. L'échange de code OAuth2 (",[26,67,68],{},"authorization_code"," → ",[26,71,28],{},") doit se faire côté serveur. Sans BFF, soit on sacrifie la sécurité, soit on complexifie le frontend avec du PKCE et des workarounds.",[18,74,76],{"id":75},"architecture-bff","Architecture BFF",[78,79,84],"pre",{"className":80,"code":82,"language":83},[81],"language-text","Navigateur (Vue.js)\n        │\n        │  Cookie de session (HttpOnly, Secure)\n        ▼\n  FastAPI BFF\n        │\n        ├─── Redis (sessions chiffrées + tokens OAuth2)\n        │\n        └─── Azure B2C (échange de codes, refresh de tokens)\n                │\n                └─── APIs métier (avec access_token en Bearer)\n","text",[26,85,82],{"__ignoreMap":86},"",[14,88,89],{},"Le navigateur ne voit jamais de token OAuth2. Il n'échange qu'un cookie de session opaque avec le BFF. C'est le BFF qui détient les tokens et les injecte dans les requêtes vers les APIs métier.",[18,91,93],{"id":92},"implémentation-fastapi","Implémentation FastAPI",[95,96,98],"h3",{"id":97},"gestion-de-session-avec-redis","Gestion de session avec Redis",[78,100,104],{"className":101,"code":102,"language":103,"meta":86,"style":86},"language-python shiki shiki-themes github-dark github-light","import json\nimport secrets\nfrom datetime import timedelta\nfrom cryptography.fernet import Fernet\nimport redis.asyncio as aioredis\nfrom fastapi import Request, Response\n\nclass SessionManager:\n    def __init__(self, redis: aioredis.Redis, secret_key: bytes):\n        self.redis = redis\n        self.fernet = Fernet(secret_key)\n        self.session_ttl = 3600  # 1 heure\n\n    def _session_key(self, session_id: str) -> str:\n        return f\"bff:session:{session_id}\"\n\n    async def create_session(self, response: Response, data: dict) -> str:\n        session_id = secrets.token_urlsafe(32)\n        encrypted = self.fernet.encrypt(json.dumps(data).encode())\n        await self.redis.setex(\n            self._session_key(session_id),\n            self.session_ttl,\n            encrypted\n        )\n        response.set_cookie(\n            key=\"session_id\",\n            value=session_id,\n            httponly=True,\n            secure=True,\n            samesite=\"lax\",\n            max_age=self.session_ttl\n        )\n        return session_id\n\n    async def get_session(self, request: Request) -> dict | None:\n        session_id = request.cookies.get(\"session_id\")\n        if not session_id:\n            return None\n        raw = await self.redis.get(self._session_key(session_id))\n        if not raw:\n            return None\n        return json.loads(self.fernet.decrypt(raw))\n","python",[26,105,106,114,120,126,132,138,144,151,157,163,169,175,181,186,192,198,203,209,215,221,227,233,239,245,251,257,263,269,275,281,287,293,298,304,309,315,321,327,333,339,345,350],{"__ignoreMap":86},[107,108,111],"span",{"class":109,"line":110},"line",1,[107,112,113],{},"import json\n",[107,115,117],{"class":109,"line":116},2,[107,118,119],{},"import secrets\n",[107,121,123],{"class":109,"line":122},3,[107,124,125],{},"from datetime import timedelta\n",[107,127,129],{"class":109,"line":128},4,[107,130,131],{},"from cryptography.fernet import Fernet\n",[107,133,135],{"class":109,"line":134},5,[107,136,137],{},"import redis.asyncio as aioredis\n",[107,139,141],{"class":109,"line":140},6,[107,142,143],{},"from fastapi import Request, Response\n",[107,145,147],{"class":109,"line":146},7,[107,148,150],{"emptyLinePlaceholder":149},true,"\n",[107,152,154],{"class":109,"line":153},8,[107,155,156],{},"class SessionManager:\n",[107,158,160],{"class":109,"line":159},9,[107,161,162],{},"    def __init__(self, redis: aioredis.Redis, secret_key: bytes):\n",[107,164,166],{"class":109,"line":165},10,[107,167,168],{},"        self.redis = redis\n",[107,170,172],{"class":109,"line":171},11,[107,173,174],{},"        self.fernet = Fernet(secret_key)\n",[107,176,178],{"class":109,"line":177},12,[107,179,180],{},"        self.session_ttl = 3600  # 1 heure\n",[107,182,184],{"class":109,"line":183},13,[107,185,150],{"emptyLinePlaceholder":149},[107,187,189],{"class":109,"line":188},14,[107,190,191],{},"    def _session_key(self, session_id: str) -> str:\n",[107,193,195],{"class":109,"line":194},15,[107,196,197],{},"        return f\"bff:session:{session_id}\"\n",[107,199,201],{"class":109,"line":200},16,[107,202,150],{"emptyLinePlaceholder":149},[107,204,206],{"class":109,"line":205},17,[107,207,208],{},"    async def create_session(self, response: Response, data: dict) -> str:\n",[107,210,212],{"class":109,"line":211},18,[107,213,214],{},"        session_id = secrets.token_urlsafe(32)\n",[107,216,218],{"class":109,"line":217},19,[107,219,220],{},"        encrypted = self.fernet.encrypt(json.dumps(data).encode())\n",[107,222,224],{"class":109,"line":223},20,[107,225,226],{},"        await self.redis.setex(\n",[107,228,230],{"class":109,"line":229},21,[107,231,232],{},"            self._session_key(session_id),\n",[107,234,236],{"class":109,"line":235},22,[107,237,238],{},"            self.session_ttl,\n",[107,240,242],{"class":109,"line":241},23,[107,243,244],{},"            encrypted\n",[107,246,248],{"class":109,"line":247},24,[107,249,250],{},"        )\n",[107,252,254],{"class":109,"line":253},25,[107,255,256],{},"        response.set_cookie(\n",[107,258,260],{"class":109,"line":259},26,[107,261,262],{},"            key=\"session_id\",\n",[107,264,266],{"class":109,"line":265},27,[107,267,268],{},"            value=session_id,\n",[107,270,272],{"class":109,"line":271},28,[107,273,274],{},"            httponly=True,\n",[107,276,278],{"class":109,"line":277},29,[107,279,280],{},"            secure=True,\n",[107,282,284],{"class":109,"line":283},30,[107,285,286],{},"            samesite=\"lax\",\n",[107,288,290],{"class":109,"line":289},31,[107,291,292],{},"            max_age=self.session_ttl\n",[107,294,296],{"class":109,"line":295},32,[107,297,250],{},[107,299,301],{"class":109,"line":300},33,[107,302,303],{},"        return session_id\n",[107,305,307],{"class":109,"line":306},34,[107,308,150],{"emptyLinePlaceholder":149},[107,310,312],{"class":109,"line":311},35,[107,313,314],{},"    async def get_session(self, request: Request) -> dict | None:\n",[107,316,318],{"class":109,"line":317},36,[107,319,320],{},"        session_id = request.cookies.get(\"session_id\")\n",[107,322,324],{"class":109,"line":323},37,[107,325,326],{},"        if not session_id:\n",[107,328,330],{"class":109,"line":329},38,[107,331,332],{},"            return None\n",[107,334,336],{"class":109,"line":335},39,[107,337,338],{},"        raw = await self.redis.get(self._session_key(session_id))\n",[107,340,342],{"class":109,"line":341},40,[107,343,344],{},"        if not raw:\n",[107,346,348],{"class":109,"line":347},41,[107,349,332],{},[107,351,353],{"class":109,"line":352},42,[107,354,355],{},"        return json.loads(self.fernet.decrypt(raw))\n",[95,357,359],{"id":358},"callback-oauth2-azure-b2c","Callback OAuth2 Azure B2C",[78,361,363],{"className":101,"code":362,"language":103,"meta":86,"style":86},"from fastapi import APIRouter, Request, Response\nfrom httpx import AsyncClient\n\nrouter = APIRouter()\n\n@router.get(\"/auth/callback\")\nasync def oauth_callback(\n    request: Request,\n    response: Response,\n    code: str,\n    state: str,\n):\n    # Échange du code contre les tokens — côté serveur uniquement\n    async with AsyncClient() as client:\n        token_response = await client.post(\n            f\"https://{settings.b2c_tenant}.b2clogin.com/\"\n            f\"{settings.b2c_tenant}.onmicrosoft.com/\"\n            f\"{settings.b2c_policy}/oauth2/v2.0/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": settings.client_id,\n                \"client_secret\": settings.client_secret,  # Jamais exposé au navigateur\n                \"code\": code,\n                \"redirect_uri\": settings.redirect_uri,\n            }\n        )\n\n    tokens = token_response.json()\n    await session_manager.create_session(response, {\n        \"access_token\": tokens[\"access_token\"],\n        \"refresh_token\": tokens[\"refresh_token\"],\n        \"expires_at\": time.time() + tokens[\"expires_in\"]\n    })\n\n    return RedirectResponse(url=\"/\")\n",[26,364,365,370,375,379,384,388,393,398,403,408,413,418,423,428,433,438,443,448,453,458,463,468,473,478,483,488,492,496,501,506,511,516,521,526,530],{"__ignoreMap":86},[107,366,367],{"class":109,"line":110},[107,368,369],{},"from fastapi import APIRouter, Request, Response\n",[107,371,372],{"class":109,"line":116},[107,373,374],{},"from httpx import AsyncClient\n",[107,376,377],{"class":109,"line":122},[107,378,150],{"emptyLinePlaceholder":149},[107,380,381],{"class":109,"line":128},[107,382,383],{},"router = APIRouter()\n",[107,385,386],{"class":109,"line":134},[107,387,150],{"emptyLinePlaceholder":149},[107,389,390],{"class":109,"line":140},[107,391,392],{},"@router.get(\"/auth/callback\")\n",[107,394,395],{"class":109,"line":146},[107,396,397],{},"async def oauth_callback(\n",[107,399,400],{"class":109,"line":153},[107,401,402],{},"    request: Request,\n",[107,404,405],{"class":109,"line":159},[107,406,407],{},"    response: Response,\n",[107,409,410],{"class":109,"line":165},[107,411,412],{},"    code: str,\n",[107,414,415],{"class":109,"line":171},[107,416,417],{},"    state: str,\n",[107,419,420],{"class":109,"line":177},[107,421,422],{},"):\n",[107,424,425],{"class":109,"line":183},[107,426,427],{},"    # Échange du code contre les tokens — côté serveur uniquement\n",[107,429,430],{"class":109,"line":188},[107,431,432],{},"    async with AsyncClient() as client:\n",[107,434,435],{"class":109,"line":194},[107,436,437],{},"        token_response = await client.post(\n",[107,439,440],{"class":109,"line":200},[107,441,442],{},"            f\"https://{settings.b2c_tenant}.b2clogin.com/\"\n",[107,444,445],{"class":109,"line":205},[107,446,447],{},"            f\"{settings.b2c_tenant}.onmicrosoft.com/\"\n",[107,449,450],{"class":109,"line":211},[107,451,452],{},"            f\"{settings.b2c_policy}/oauth2/v2.0/token\",\n",[107,454,455],{"class":109,"line":217},[107,456,457],{},"            data={\n",[107,459,460],{"class":109,"line":223},[107,461,462],{},"                \"grant_type\": \"authorization_code\",\n",[107,464,465],{"class":109,"line":229},[107,466,467],{},"                \"client_id\": settings.client_id,\n",[107,469,470],{"class":109,"line":235},[107,471,472],{},"                \"client_secret\": settings.client_secret,  # Jamais exposé au navigateur\n",[107,474,475],{"class":109,"line":241},[107,476,477],{},"                \"code\": code,\n",[107,479,480],{"class":109,"line":247},[107,481,482],{},"                \"redirect_uri\": settings.redirect_uri,\n",[107,484,485],{"class":109,"line":253},[107,486,487],{},"            }\n",[107,489,490],{"class":109,"line":259},[107,491,250],{},[107,493,494],{"class":109,"line":265},[107,495,150],{"emptyLinePlaceholder":149},[107,497,498],{"class":109,"line":271},[107,499,500],{},"    tokens = token_response.json()\n",[107,502,503],{"class":109,"line":277},[107,504,505],{},"    await session_manager.create_session(response, {\n",[107,507,508],{"class":109,"line":283},[107,509,510],{},"        \"access_token\": tokens[\"access_token\"],\n",[107,512,513],{"class":109,"line":289},[107,514,515],{},"        \"refresh_token\": tokens[\"refresh_token\"],\n",[107,517,518],{"class":109,"line":295},[107,519,520],{},"        \"expires_at\": time.time() + tokens[\"expires_in\"]\n",[107,522,523],{"class":109,"line":300},[107,524,525],{},"    })\n",[107,527,528],{"class":109,"line":306},[107,529,150],{"emptyLinePlaceholder":149},[107,531,532],{"class":109,"line":311},[107,533,534],{},"    return RedirectResponse(url=\"/\")\n",[95,536,538],{"id":537},"proxy-vers-les-apis-métier","Proxy vers les APIs métier",[78,540,542],{"className":101,"code":541,"language":103,"meta":86,"style":86},"@router.get(\"/api/{path:path}\")\nasync def proxy(request: Request, path: str):\n    session = await session_manager.get_session(request)\n    if not session:\n        raise HTTPException(status_code=401)\n\n    # Refresh automatique si le token est expiré\n    if time.time() > session[\"expires_at\"] - 60:\n        session = await token_refresher.refresh(session)\n\n    async with AsyncClient() as client:\n        response = await client.request(\n            method=request.method,\n            url=f\"{settings.api_base_url}/{path}\",\n            headers={\"Authorization\": f\"Bearer {session['access_token']}\"},\n            content=await request.body(),\n        )\n\n    return Response(\n        content=response.content,\n        status_code=response.status_code,\n        media_type=response.headers.get(\"content-type\")\n    )\n",[26,543,544,549,554,559,564,569,573,578,583,588,592,596,601,606,611,616,621,625,629,634,639,644,649],{"__ignoreMap":86},[107,545,546],{"class":109,"line":110},[107,547,548],{},"@router.get(\"/api/{path:path}\")\n",[107,550,551],{"class":109,"line":116},[107,552,553],{},"async def proxy(request: Request, path: str):\n",[107,555,556],{"class":109,"line":122},[107,557,558],{},"    session = await session_manager.get_session(request)\n",[107,560,561],{"class":109,"line":128},[107,562,563],{},"    if not session:\n",[107,565,566],{"class":109,"line":134},[107,567,568],{},"        raise HTTPException(status_code=401)\n",[107,570,571],{"class":109,"line":140},[107,572,150],{"emptyLinePlaceholder":149},[107,574,575],{"class":109,"line":146},[107,576,577],{},"    # Refresh automatique si le token est expiré\n",[107,579,580],{"class":109,"line":153},[107,581,582],{},"    if time.time() > session[\"expires_at\"] - 60:\n",[107,584,585],{"class":109,"line":159},[107,586,587],{},"        session = await token_refresher.refresh(session)\n",[107,589,590],{"class":109,"line":165},[107,591,150],{"emptyLinePlaceholder":149},[107,593,594],{"class":109,"line":171},[107,595,432],{},[107,597,598],{"class":109,"line":177},[107,599,600],{},"        response = await client.request(\n",[107,602,603],{"class":109,"line":183},[107,604,605],{},"            method=request.method,\n",[107,607,608],{"class":109,"line":188},[107,609,610],{},"            url=f\"{settings.api_base_url}/{path}\",\n",[107,612,613],{"class":109,"line":194},[107,614,615],{},"            headers={\"Authorization\": f\"Bearer {session['access_token']}\"},\n",[107,617,618],{"class":109,"line":200},[107,619,620],{},"            content=await request.body(),\n",[107,622,623],{"class":109,"line":205},[107,624,250],{},[107,626,627],{"class":109,"line":211},[107,628,150],{"emptyLinePlaceholder":149},[107,630,631],{"class":109,"line":217},[107,632,633],{},"    return Response(\n",[107,635,636],{"class":109,"line":223},[107,637,638],{},"        content=response.content,\n",[107,640,641],{"class":109,"line":229},[107,642,643],{},"        status_code=response.status_code,\n",[107,645,646],{"class":109,"line":235},[107,647,648],{},"        media_type=response.headers.get(\"content-type\")\n",[107,650,651],{"class":109,"line":241},[107,652,653],{},"    )\n",[18,655,657],{"id":656},"verrou-distribué-pour-le-refresh-de-token","Verrou distribué pour le refresh de token",[14,659,660],{},"En environnement multi-pods, plusieurs workers peuvent tenter de rafraîchir le même token simultanément. Un verrou Redis évite les doublons :",[78,662,664],{"className":101,"code":663,"language":103,"meta":86,"style":86},"async def refresh(self, session: dict) -> dict:\n    lock_key = f\"bff:lock:refresh:{session['user_id']}\"\n\n    async with self.redis.lock(lock_key, timeout=10, blocking_timeout=8):\n        # Re-lire la session : peut-être déjà rafraîchie par un autre pod\n        fresh = await self.session_manager.get_session_by_user(session[\"user_id\"])\n        if time.time() \u003C fresh[\"expires_at\"] - 60:\n            return fresh  # Déjà rafraîchi, rien à faire\n\n        # Effectuer le refresh\n        async with AsyncClient() as client:\n            response = await client.post(\n                f\"{settings.token_endpoint}\",\n                data={\n                    \"grant_type\": \"refresh_token\",\n                    \"refresh_token\": fresh[\"refresh_token\"],\n                    \"client_id\": settings.client_id,\n                    \"client_secret\": settings.client_secret,\n                }\n            )\n        new_tokens = response.json()\n        updated_session = {**fresh, **new_tokens, \"expires_at\": time.time() + new_tokens[\"expires_in\"]}\n        await self.session_manager.update_session(updated_session)\n        return updated_session\n",[26,665,666,671,676,680,685,690,695,700,705,709,714,719,724,729,734,739,744,749,754,759,764,769,774,779],{"__ignoreMap":86},[107,667,668],{"class":109,"line":110},[107,669,670],{},"async def refresh(self, session: dict) -> dict:\n",[107,672,673],{"class":109,"line":116},[107,674,675],{},"    lock_key = f\"bff:lock:refresh:{session['user_id']}\"\n",[107,677,678],{"class":109,"line":122},[107,679,150],{"emptyLinePlaceholder":149},[107,681,682],{"class":109,"line":128},[107,683,684],{},"    async with self.redis.lock(lock_key, timeout=10, blocking_timeout=8):\n",[107,686,687],{"class":109,"line":134},[107,688,689],{},"        # Re-lire la session : peut-être déjà rafraîchie par un autre pod\n",[107,691,692],{"class":109,"line":140},[107,693,694],{},"        fresh = await self.session_manager.get_session_by_user(session[\"user_id\"])\n",[107,696,697],{"class":109,"line":146},[107,698,699],{},"        if time.time() \u003C fresh[\"expires_at\"] - 60:\n",[107,701,702],{"class":109,"line":153},[107,703,704],{},"            return fresh  # Déjà rafraîchi, rien à faire\n",[107,706,707],{"class":109,"line":159},[107,708,150],{"emptyLinePlaceholder":149},[107,710,711],{"class":109,"line":165},[107,712,713],{},"        # Effectuer le refresh\n",[107,715,716],{"class":109,"line":171},[107,717,718],{},"        async with AsyncClient() as client:\n",[107,720,721],{"class":109,"line":177},[107,722,723],{},"            response = await client.post(\n",[107,725,726],{"class":109,"line":183},[107,727,728],{},"                f\"{settings.token_endpoint}\",\n",[107,730,731],{"class":109,"line":188},[107,732,733],{},"                data={\n",[107,735,736],{"class":109,"line":194},[107,737,738],{},"                    \"grant_type\": \"refresh_token\",\n",[107,740,741],{"class":109,"line":200},[107,742,743],{},"                    \"refresh_token\": fresh[\"refresh_token\"],\n",[107,745,746],{"class":109,"line":205},[107,747,748],{},"                    \"client_id\": settings.client_id,\n",[107,750,751],{"class":109,"line":211},[107,752,753],{},"                    \"client_secret\": settings.client_secret,\n",[107,755,756],{"class":109,"line":217},[107,757,758],{},"                }\n",[107,760,761],{"class":109,"line":223},[107,762,763],{},"            )\n",[107,765,766],{"class":109,"line":229},[107,767,768],{},"        new_tokens = response.json()\n",[107,770,771],{"class":109,"line":235},[107,772,773],{},"        updated_session = {**fresh, **new_tokens, \"expires_at\": time.time() + new_tokens[\"expires_in\"]}\n",[107,775,776],{"class":109,"line":241},[107,777,778],{},"        await self.session_manager.update_session(updated_session)\n",[107,780,781],{"class":109,"line":247},[107,782,783],{},"        return updated_session\n",[18,785,787],{"id":786},"ce-que-le-bff-apporte","Ce que le BFF apporte",[789,790,791,807],"table",{},[792,793,794],"thead",{},[795,796,797,801,804],"tr",{},[798,799,800],"th",{},"Aspect",[798,802,803],{},"Sans BFF",[798,805,806],{},"Avec BFF",[808,809,810,822,834,845,856],"tbody",{},[795,811,812,816,819],{},[813,814,815],"td",{},"Tokens dans le navigateur",[813,817,818],{},"Oui (localStorage / cookie)",[813,820,821],{},"Non — jamais exposés",[795,823,824,828,831],{},[813,825,826],{},[26,827,64],{},[813,829,830],{},"Absent (PKCE requis)",[813,832,833],{},"Côté serveur uniquement",[795,835,836,839,842],{},[813,837,838],{},"Refresh token",[813,840,841],{},"Géré par le frontend",[813,843,844],{},"Géré par le BFF avec verrou",[795,846,847,850,853],{},[813,848,849],{},"Surface d'attaque XSS",[813,851,852],{},"Tokens accessibles",[813,854,855],{},"Cookie opaque uniquement",[795,857,858,861,864],{},[813,859,860],{},"Complexité",[813,862,863],{},"Frontend complexe",[813,865,866],{},"Backend supplémentaire",[14,868,869],{},"Le BFF n'est pas la solution universelle. Sur une application publique sans données sensibles, PKCE côté client est suffisant et plus simple à opérer. Mais dès que l'on gère des tokens avec des droits élevés, des scopes sensibles, ou des intégrations multi-APIs, le BFF est l'architecture la plus défendable.",[871,872,873],"style",{},"html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":86,"searchDepth":116,"depth":116,"links":875},[876,877,878,883,884],{"id":20,"depth":116,"text":21},{"id":75,"depth":116,"text":76},{"id":92,"depth":116,"text":93,"children":879},[880,881,882],{"id":97,"depth":122,"text":98},{"id":358,"depth":122,"text":359},{"id":537,"depth":122,"text":538},{"id":656,"depth":116,"text":657},{"id":786,"depth":116,"text":787},"2025-02-13",null,"md",{},"/fr/blog/pattern-bff",{"title":5,"description":16},"pattern-bff","fr/blog/pattern-bff",[894,895,896,897,898],"FastAPI","Vue.js","Azure B2C","Architecture","OAuth2","SJJwnE2yXDRfPzvEucoNBa0YZ_QcH3ikhrvNoC1EjaI",1774645635862]