CSP Configuration
In this page we will see how to relax your Content-Security-Policy just enough so session restoration using iframe is possible.
CSP rules that break session restoration via iframe
Silent session restoration via iframe is optional, oidc-spa can restore user session just fine using full page redirect.
If that is so, why does oidc-spa even attemt to use iframe?
For performance: iframe based session resoration is a little faster than full page redirect (not much but still noticable).
For security if and only if your app talks to more than one resource server (which is not the case in most apps), because "multiple oidc-client" + "no iframe SSO" = "oidc-spa needs to persist tokens in session storage".
1) Your app cannot iframe the IdP
This happens when:
X-Frame-Options: DENYContent-Security-Policy: frame-src 'none'Content-Security-Policy: frame-src 'self'Content-Security-Policy: frame-src 'self' https://not-my-idp.com
If the IdP domain is missing from frame-src, the iframe cannot load → silent SSO cannot run.
2) Your app cannot be iframed by itself
Silent SSO needs to temporarily load your app inside an iframe (when the IdP redirects back with the authorization response).
If you block this:
Content-Security-Policy: frame-ancestors 'none'
…then the IdP cannot redirect to your app inside the iframe → silent SSO fails.
How to fix it
To restore silent SSO:
Remove any
X-Frame-Optionsheader (deprecated).Allow the IdP domain in
frame-src.Allow your app to frame itself using
frame-ancestors 'self'.
Example:
Content-Security-Policy:
frame-src https://auth.my-domain.com;
frame-ancestors 'self';
...other CSP directives...Tip: Instead of hardcoding the IdP domain, allow sibling subdomains of your app’s domain. This stays aligned with same-site cookie rules and avoids config drift between environments.
The Nginx configuration below demonstrates this pattern.
Canonical Nginx configuration
This is a portable, production-grade Ngnix config for an SPA with "as strict as can be" CSPs that still enable oidc-spa to use iframe for performing session restoration.
Of course you can relax thoses CSP to meet the specific need of your app. This is just an example of what you would use if you use no service/web workers and don't have any inline script.
# Assuming nginxinc/nginx-unprivileged
# ============================================================
# Dynamic base domain extraction (per request)
# Example: dashboard.my-company.com -> my-company.com
# ============================================================
map $host $base_domain {
~^(?<sub>.+)\.(?<domain>[^.]+\.[^.]+)$ $domain;
default $host;
}
server {
listen 8080;
# -------------------------
# Gzip
# -------------------------
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types
text/plain text/css text/xml text/javascript
application/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";
# -------------------------
# Root and SPA routing
# -------------------------
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# -------------------------
# Vite hashed assets (cache 1 year)
# -------------------------
location ^~ /assets/ {
try_files $uri =404;
expires 1y;
access_log off;
add_header Cache-Control "public" always;
}
# -------------------------
# HTML (never cached) + CSP
# -------------------------
location ~* \.html$ {
try_files $uri =404;
expires -1;
add_header Content-Security-Policy
"frame-src https://*.$base_domain https://$base_domain; "
"frame-ancestors 'self'; "
"object-src 'none'; "
"worker-src 'none'; "
"child-src 'none'; "
"script-src 'self' 'strict-dynamic';"
always;
}
# -------------------------
# JSON / TXT (never cached)
# -------------------------
location ~* \.(json|txt)$ {
try_files $uri =404;
expires -1;
}
# -------------------------
# Any other file with an extension (cache 1 day)
# -------------------------
location ~ ^.+\..+$ {
try_files $uri =404;
expires 1d;
access_log off;
add_header Cache-Control "public" always;
}
}Last updated
Was this helpful?