{"id":21,"date":"2026-06-06T21:48:45","date_gmt":"2026-06-06T21:48:45","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=21"},"modified":"2026-06-06T23:45:13","modified_gmt":"2026-06-06T23:45:13","slug":"security-in-a-nuxt-ssr-app-csrf-oauth-csp-and-more","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=21","title":{"rendered":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Fifteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Security in SSR Is Different<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An SSR application has a very different attack surface from a client-side SPA. The server is responsible for rendering HTML with embedded state, generating tokens, setting cookies, and proxying API calls \u2014 all before the browser executes any JavaScript.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Security must be enforced at the <strong>server rendering layer<\/strong>. A CSRF token created during SSR has to survive hydration. Authentication must block the HTTP response before it ever reaches the browser. CSP must be sent as an HTTP header during rendering, not injected later as a meta tag.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Reusing SPA security patterns directly in SSR apps creates gaps \u2014 not because the patterns are wrong, but because they operate at the wrong layer.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Client[\"Browser\"]\n    HTML[\"SSR HTML + Embedded State\"]\n    JS[\"Hydrated JS App\"]\n  end\n\n  subgraph Server[\"Nuxt SSR Stack\"]\n    Render[\"SSR Render Layer\"]\n    Tokens[\"Token Generation&lt;br\/&gt;(CSRF, Auth)\"]\n    Cookies[\"Set Cookies&lt;br\/&gt;(HTTP-only, SameSite)\"]\n    Proxy[\"API Proxy \/ BFF\"]\n  end\n\n  Client &lt;-- \"HTTP Response\" --&gt; Server\n  Render --&gt; HTML\n  Render --&gt; Tokens\n  Tokens --&gt; Cookies\n  Render --&gt; Proxy\n  Proxy --&gt;|\"Internal API Calls\"| Backend[\"Upstream APIs \/ Services\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">CSRF Protection: Dual-Token System with User-Agent Binding<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">CSRF protection in an SSR app needs more than the classic double-submit cookie pattern.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Standard Pattern (And Why It&#8217;s Not Enough)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The traditional double-submit approach: generate a token, store it in a cookie, and require it in a request header. The server verifies that the cookie and header values match. This works because a cross-site attacker cannot read the cookie in order to set the matching header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The weakness: if an attacker somehow gets both the cookie and the token (for example, via a subdomain cookie issue or XSS on a related domain), they can replay the request from any browser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Enhanced Pattern: User-Agent Binding<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is to <strong>bind the token to the specific browser that requested it<\/strong>:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  subgraph SSR[\"Token Generation During SSR\"]\n    UA[\"User-Agent Header\"]\n    Time[\"Current Timestamp\"]\n    Salt[\"Random UUID Salt\"]\n    Type[\"Token Type = 'client'\"]\n    HashUA[\"SHA-256(User-Agent)&lt;br\/&gt;\u2192 first 16 chars\"]\n    Payload[\"Token Payload&lt;br\/&gt;d: timestamp&lt;br\/&gt;p: type&lt;br\/&gt;s: salt&lt;br\/&gt;ua: UA hash\"]\n    Enc[\"Encrypt with AES-256-GCM\"]\n\n    UA --&gt; HashUA\n    Time --&gt; Payload\n    Type --&gt; Payload\n    Salt --&gt; Payload\n    HashUA --&gt; Payload\n    Payload --&gt; Enc\n  end\n\n  Enc --&gt; Cookie[\"Set HTTP-only cookie: csrf\"]\n  Enc --&gt; Embed[\"Embed token in SSR HTML&lt;br\/&gt;(for X-XSRF-TOKEN header)\"]<\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  subgraph Validation[\"Token Validation on Every API Request\"]\n    HeaderTok[\"X-XSRF-TOKEN header\"]\n    CookieTok[\"csrf cookie\"]\n    Decrypt[\"Decrypt header token\"]\n    Exp[\"Check expiration (24h TTL)\"]\n    ReqUA[\"Current User-Agent\"]\n    HashReqUA[\"SHA-256(Req UA)&lt;br\/&gt;\u2192 first 16 chars\"]\n    MatchUA[\"Compare ua in token&lt;br\/&gt;with current UA hash\"]\n    MatchCookie[\"Compare cookie value&lt;br\/&gt;with header value\"]\n    Ok[\"All checks pass\"]\n    Reject[\"Reject 403 Forbidden\"]\n\n    HeaderTok --&gt; Decrypt\n    Decrypt --&gt; Exp\n    Decrypt --&gt; MatchUA\n    ReqUA --&gt; HashReqUA --&gt; MatchUA\n    Decrypt --&gt; MatchCookie\n    CookieTok --&gt; MatchCookie\n\n    Exp --&gt;|valid| MatchUA\n    Exp --&gt;|expired| Reject\n    MatchUA --&gt;|mismatch| Reject\n    MatchUA --&gt;|match| MatchCookie\n    MatchCookie --&gt;|mismatch| Reject\n    MatchCookie --&gt;|match| Ok\n    Ok --&gt;|\"Process API request\"| App[\"Application Handler\"]\n  end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A stolen token is useless from a different browser \u2014 the User-Agent hash will not match. Combined with AES-256-GCM encryption, random salts, and a 24-hour TTL, this creates layered defenses against replay attacks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SSR Bypass Tokens<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">During SSR, the Nuxt server calls its own GraphQL or REST APIs \u2014 there is no browser, and therefore no CSRF cookie. A <strong>server-only bypass token<\/strong> allows these internal SSR requests to pass CSRF checks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This token is generated per request, stored only in the Nitro event context (never exposed to the client), and validated using User-Agent binding but without the cookie\u2013header comparison.<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n  participant Browser\n  participant NuxtSSR as Nuxt SSR Renderer\n  participant NitroCtx as Nitro Event Context\n  participant InternalAPI as Internal API\n\n  Browser-&gt;&gt;NuxtSSR: HTTP GET \/page\n  NuxtSSR-&gt;&gt;NitroCtx: Create SSR bypass token&lt;br\/&gt;(bound to UA, no cookie)\n  Note right of NitroCtx: Token stored only in&lt;br\/&gt;server context, not sent&lt;br\/&gt;to the client\n  NuxtSSR-&gt;&gt;InternalAPI: Request with SSR bypass token&lt;br\/&gt;(e.g. header X-SSR-CSRF)\n  InternalAPI--&gt;&gt;InternalAPI: Validate token + UA&lt;br\/&gt;(no cookie-header check)\n  InternalAPI--&gt;&gt;NuxtSSR: Data response\n  NuxtSSR--&gt;&gt;Browser: Rendered HTML<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Encryption Key from Key Vault<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The AES key is loaded from a cloud key vault (for example, Azure Key Vault or AWS KMS) at startup and stored on <code>globalThis<\/code>. If the key cannot be loaded, the server refuses to start \u2014 a fail-fast approach with no degraded mode. CSRF protection is never quietly turned off.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Startup[\"Nuxt Server Startup\"]\n    KV[\"Cloud Key Vault&lt;br\/&gt;(AWS KMS \/ Azure Key Vault)\"]\n    Fetch[\"Fetch AES-256 Key\"]\n    Store[\"Store key on globalThis\"]\n    Ready[\"Server Ready\"]\n    Fail[\"Abort startup&lt;br\/&gt;(process exit)\"]\n\n    KV --&gt; Fetch\n    Fetch --&gt;|success| Store --&gt; Ready\n    Fetch --&gt;|failure| Fail\n  end<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">OAuth 2.0 Authentication (Authorization Code Flow)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test and staging environments often require authentication, even for otherwise public-facing applications. A common pattern is server-side OAuth 2.0 Authorization Code Flow \u2014 the client secret is never exposed to the browser.<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n  participant Browser\n  participant Nuxt as Nuxt Server\n  participant IdP as Identity Provider\n\n  Browser-&gt;&gt;Nuxt: GET \/protected-page\n  Nuxt--&gt;&gt;Nuxt: Check auth cookie\n  alt No valid token\n    Nuxt--&gt;&gt;Browser: 302 Redirect to \/api\/auth\/login\n    Browser-&gt;&gt;Nuxt: GET \/api\/auth\/login\n    Nuxt--&gt;&gt;Browser: 302 Redirect to IdP auth URL\n    Browser-&gt;&gt;IdP: GET \/authorize?client_id=...&amp;redirect_uri=\/api\/auth\/callback\n    IdP--&gt;&gt;Browser: 302 Redirect to \/api\/auth\/callback?code=...\n    Browser-&gt;&gt;Nuxt: GET \/api\/auth\/callback?code=...\n    Nuxt-&gt;&gt;IdP: POST \/token (exchange code for token)\n    IdP--&gt;&gt;Nuxt: Access token\n    Nuxt--&gt;&gt;Browser: Set HTTP-only auth cookie + 302 \/protected-page\n    Browser-&gt;&gt;Nuxt: GET \/protected-page\n    Nuxt--&gt;&gt;Browser: 200 Full HTML\n  else Valid token\n    Nuxt--&gt;&gt;Browser: 200 Full HTML (no redirect)\n  end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Key security properties:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Server-side token exchange<\/strong> \u2014 the client secret is used only on the server, never sent to the browser.<\/li>\n<li><strong>HTTP-only cookies<\/strong> \u2014 tokens live in cookies that JavaScript cannot read (mitigating XSS).<\/li>\n<li><strong>Middleware blocking<\/strong> \u2014 unauthenticated requests are stopped in server middleware before any page content is rendered. Unauthorized users cannot even download the app\u2019s JavaScript bundle.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Environment-Based Security Tiers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not every environment needs the full security stack:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Environment<\/th><th>CSRF<\/th><th>Auth<\/th><th>CSP<\/th><th>Rationale<\/th><\/tr><\/thead><tbody><tr><td>Development<\/td><td>Off<\/td><td>Off<\/td><td>Off<\/td><td>Fast iteration, minimal friction<\/td><\/tr><tr><td>Docker<\/td><td>Off<\/td><td>On<\/td><td>Off<\/td><td>Protects shared dev environments<\/td><\/tr><tr><td>Test<\/td><td>On<\/td><td>On<\/td><td>On<\/td><td>Production-like security<\/td><\/tr><tr><td>Production<\/td><td>On<\/td><td>Off<\/td><td>On<\/td><td>Public site, no login required<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Development turns security off for productivity. Test enables everything to catch issues before release. Production enables CSRF and CSP but omits authentication for a public site.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  Dev[\"Development\"]:::off --&gt;|Deploy| Docker[\"Docker \/ Shared Dev\"]:::partial\n  Docker --&gt;|Promote| Test[\"Test \/ Staging\"]:::full\n  Test --&gt;|Promote| Prod[\"Production\"]:::prod\n\n  classDef off fill:#eee,stroke:#999,color:#333;\n  classDef partial fill:#ffe6b3,stroke:#cc9a00,color:#333;\n  classDef full fill:#c6f6d5,stroke:#2f855a,color:#000;\n  classDef prod fill:#bee3f8,stroke:#2b6cb0,color:#000;\n\n  Dev --- DevSec[\"CSRF: Off&lt;br\/&gt;Auth: Off&lt;br\/&gt;CSP: Off\"]\n  Docker --- DockSec[\"CSRF: Off&lt;br\/&gt;Auth: On&lt;br\/&gt;CSP: Off\"]\n  Test --- TestSec[\"CSRF: On&lt;br\/&gt;Auth: On&lt;br\/&gt;CSP: On\"]\n  Prod --- ProdSec[\"CSRF: On&lt;br\/&gt;Auth: Off&lt;br\/&gt;CSP: On\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Runtime Content Security Policy from CMS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardcoding CSP in application config is an operational choke point: every new script source (analytics, chat widgets, A\/B testing) forces a code change and deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Treating CSP as content solves this. A server plugin fetches CSP from the CMS at runtime and applies it as an HTTP header:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  Editor[\"CMS Editor&lt;br\/&gt;updates CSP entry\"] --&gt; CMS[\"Headless CMS\"]\n  CMS --&gt; Cache[\"Server Plugin&lt;br\/&gt;fetches CSP (cache 5 min)\"]\n  Cache --&gt; Header[\"Set Content-Security-Policy&lt;br\/&gt;header on HTML responses\"]\n  Header --&gt; Browser[\"Browser enforces CSP\"]\n\n  CMS -.failure.-&gt; Fallback[\"Use hardcoded fallback CSP&lt;br\/&gt;(stricter, not looser)\"]\n  Fallback --&gt; Header<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A hardcoded fallback covers CMS downtime. The runtime CSP is strictly more permissive than the fallback \u2014 if the CMS fetch fails, the app operates under a <strong>stricter<\/strong>, not looser, policy.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Internal API Guard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Health and diagnostics endpoints expose sensitive operational data \u2014 memory usage, restart counts, worker status. The Internal API Guard keeps these endpoints invisible to the public:<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n  participant PublicClient as External Client\n  participant Probe as Kube Health Probe\n  participant InternalSvc as Internal Service\n  participant NuxtAPI as Nuxt API Layer\n\n  PublicClient-&gt;&gt;NuxtAPI: GET \/api\/health\/pm2&lt;br\/&gt;User-Agent: Mozilla\/5.0\n  NuxtAPI--&gt;&gt;PublicClient: 404 Not Found\n\n  Probe-&gt;&gt;NuxtAPI: GET \/api\/health&lt;br\/&gt;User-Agent: kube-probe\/1.28\n  NuxtAPI--&gt;&gt;Probe: 200 OK { status: \"healthy\" }\n\n  InternalSvc-&gt;&gt;NuxtAPI: GET \/api\/health\/pm2&lt;br\/&gt;X-Internal-Secret: correct\n  NuxtAPI--&gt;&gt;InternalSvc: 200 OK { workers: [...] }<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>404<\/code> is deliberate \u2014 it neither confirms nor denies the endpoint\u2019s existence. Scanners see exactly what they would for any non-existent path.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Security Stack<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All layers combine into a single request pipeline:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  Browser[\"Browser\"] --&gt; HTTPS[\"HTTPS\"]\n  HTTPS --&gt; Edge[\"Edge Load Balancer\"]\n  Edge --&gt;|\"TLS termination\"| Nginx[\"Nginx Proxy\"]\n\n  Nginx --&gt; Guard[\"Internal API Guard&lt;br\/&gt;(hide internal endpoints)\"]\n  Guard --&gt; Nuxt[\"Nuxt Server\"]\n\n  subgraph NuxtPipeline[\"Nuxt Middleware &amp; Plugins\"]\n    Auth[\"Auth Middleware&lt;br\/&gt;Check auth cookie&lt;br\/&gt;Redirect if invalid\"]\n    CSRF[\"CSRF Middleware&lt;br\/&gt;Decrypt token&lt;br\/&gt;Validate UA hash&lt;br\/&gt;Check cookie-header match\"]\n    CSP[\"CSP Plugin&lt;br\/&gt;Fetch CSP from CMS&lt;br\/&gt;Set CSP header\"]\n    SSR[\"SSR Render&lt;br\/&gt;Generate CSRF token&lt;br\/&gt;Set HTTP-only cookie&lt;br\/&gt;Embed token in HTML\"]\n  end\n\n  Nuxt --&gt; Auth --&gt; CSRF --&gt; CSP --&gt; SSR<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">SSR security operates at the HTTP response level, not the DOM level<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In a SPA, security typically lives in JavaScript \u2014 interceptors, route guards, client-side middleware. In SSR, it must live in <strong>server middleware<\/strong> controlling the HTTP response. By the time browser JavaScript runs, the HTML (and any injected payloads) has already been sent.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph SPA[\"SPA Model\"]\n    JSClient[\"Client-side JS&lt;br\/&gt;(route guards, interceptors)\"]\n    API[\"APIs\"]\n    JSClient --&gt; API\n  end\n\n  subgraph SSR[\"SSR Model\"]\n    Middleware[\"Server Middleware&lt;br\/&gt;(auth, CSRF, CSP)\"]\n    Render[\"SSR Render\"]\n    API2[\"APIs\"]\n    Middleware --&gt; Render --&gt; API2\n  end<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">User-Agent binding is cheap insurance against replay attacks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Hashing the first 16 characters of the User-Agent costs almost nothing (one SHA-256 per request) but shuts down an entire class of replay attacks. The User-Agent is available on every request \u2014 binding to it is practically free.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  UA[\"User-Agent string\"] --&gt; Hash[\"SHA-256 + truncate 16 chars\"]\n  Hash --&gt; Bind[\"Include hash in token payload\"]\n  Bind --&gt; Verify[\"On request, recompute hash&lt;br\/&gt;and compare before processing\"]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Runtime security configuration reduces operational bottlenecks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Any security setting that requires a deployment to change becomes a bottleneck. CSP changes are often requested by marketing (new script providers) and security (removing outdated sources). Moving CSP to the CMS takes the development team out of this loop.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  Marketing[\"Marketing \/ Security\"] --&gt; ChangeReq[\"Request CSP change\"]\n  ChangeReq --&gt; CMSConfig[\"Update CSP in CMS\"]\n  CMSConfig --&gt; AutoApply[\"Server auto-applies CSP&lt;br\/&gt;on next cache refresh\"]\n  AutoApply --&gt; LiveSite[\"Live Site with updated policy\"]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Environment tiers prevent security theater<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Full security in development forces engineers to work around it. Zero security in production is reckless. A tiered approach \u2014 each environment enabling exactly the protections it needs \u2014 balances safety with productivity.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n  Dev[\"Dev: Minimal security&lt;br\/&gt;High productivity\"] --&gt; Docker[\"Shared Dev: Auth only\"]\n  Docker --&gt; Test[\"Test: Full security&lt;br\/&gt;Pre-prod hardening\"]\n  Test --&gt; Prod[\"Prod: Public-friendly&lt;br\/&gt;CSRF + CSP only\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 13<\/strong>: <em>Observability and Distributed Tracing \u2014 Application Insights End-to-End<\/em> \u2014 How every request is traced across all layers.<\/li>\n<li><strong>Article 14<\/strong>: <em>AI-Assisted Development \u2014 MCP, Debug Chatbot, and the Shared Language of the Codebase<\/em> \u2014 Making AI assistants genuinely useful for live debugging.<\/li>\n<li><strong>Article 15<\/strong>: <em>Load Testing Results \u2014 15\u00d7 Faster, 5\u00d7 More Capacity<\/em> \u2014 The measured proof that architecture decisions produce real outcomes.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Fifteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Security in SSR Is Different An SSR application has a very different attack surface from a client-side SPA. The server is responsible for rendering HTML with embedded state, generating tokens, setting cookies, and proxying API calls \u2014 all before the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":238,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-21","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=21\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production\" \/>\n<meta property=\"og:description\" content=\"Fifteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Security in SSR Is Different An SSR application has a very different attack surface from a client-side SPA. The server is responsible for rendering HTML with embedded state, generating tokens, setting cookies, and proxying API calls \u2014 all before the [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=21\" \/>\n<meta property=\"og:site_name\" content=\"Scalable Web Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:48:45+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-06T23:45:13+00:00\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More\",\"datePublished\":\"2026-06-06T21:48:45+00:00\",\"dateModified\":\"2026-06-06T23:45:13+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21\"},\"wordCount\":983,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/15-security-in-nuxt-ssr.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21\",\"name\":\"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/15-security-in-nuxt-ssr.jpg\",\"datePublished\":\"2026-06-06T21:48:45+00:00\",\"dateModified\":\"2026-06-06T23:45:13+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=21\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/15-security-in-nuxt-ssr.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/15-security-in-nuxt-ssr.jpg\",\"width\":1880,\"height\":1102},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=21#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=21","og_locale":"en_US","og_type":"article","og_title":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production","og_description":"Fifteenth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Security in SSR Is Different An SSR application has a very different attack surface from a client-side SPA. The server is responsible for rendering HTML with embedded state, generating tokens, setting cookies, and proxying API calls \u2014 all before the [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=21","og_site_name":"Scalable Web Production","article_published_time":"2026-06-06T21:48:45+00:00","article_modified_time":"2026-06-06T23:45:13+00:00","author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=21#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=21"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More","datePublished":"2026-06-06T21:48:45+00:00","dateModified":"2026-06-06T23:45:13+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=21"},"wordCount":983,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=21#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/15-security-in-nuxt-ssr.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=21#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=21","url":"https:\/\/softwareproduction.eu\/?p=21","name":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More - Scalable Web Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=21#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=21#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/15-security-in-nuxt-ssr.jpg","datePublished":"2026-06-06T21:48:45+00:00","dateModified":"2026-06-06T23:45:13+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=21#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=21"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=21#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/15-security-in-nuxt-ssr.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/15-security-in-nuxt-ssr.jpg","width":1880,"height":1102},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=21#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"Security in a Nuxt SSR App \u2014 CSRF, OAuth, CSP, and More"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/15-security-in-nuxt-ssr.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/21","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=21"}],"version-history":[{"count":7,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/21\/revisions"}],"predecessor-version":[{"id":239,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/21\/revisions\/239"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/238"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=21"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=21"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=21"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}