By header

CORS Done Right: Access-Control-Allow-Origin Without the Wildcard

4 min read

CORS lets a browser on one origin call your API on another. The danger is over-sharing: Access-Control-Allow-Origin: * exposes your API to every site, and * cannot be combined with credentials. The safe pattern is to allow one (or a checked list of) origin(s).

Simple: allow ONE known origin

If only https://app.example.com should call your API:

Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Nginx:

add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;

Apache:

Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Vary "Origin"

Allow a LIST of origins (reflect if matched)

Nginx using a map (only echoes the origin if it’s allowed):

map $http_origin $cors_ok {
    default "";
    "https://app.example.com"  $http_origin;
    "https://admin.example.com" $http_origin;
}

server {
    add_header Access-Control-Allow-Origin $cors_ok always;
    add_header Vary "Origin" always;
}

With credentials (cookies / auth)

You must name a specific origin (never *) and add:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Preflight (OPTIONS) for non-simple requests

Browsers send an OPTIONS preflight for custom methods/headers. Answer it:

if ($request_method = OPTIONS) {
    add_header Access-Control-Allow-Origin $cors_ok always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    add_header Access-Control-Max-Age 86400 always;
    return 204;
}

Verify

curl -sI -H "Origin: https://app.example.com" https://api.yourdomain.com | grep -i access-control

Never do Access-Control-Allow-Origin: * on an authenticated API, and never blindly reflect $http_origin without a check — that’s the same as *. Always pair a reflected origin with Vary: Origin so caches don’t serve one origin’s CORS response to another.

Open the full version (with copy buttons) ↗

← All recipes