Authentication and CORS
A decoupled client reading more than public content has to authenticate, and a browser client on its own origin has to clear CORS first. This page covers cookie versus token authentication, where the OAuth modules sit, and how to enable CORS in services.yml.
Let a front end on a different origin authenticate to Drupal and call JSON:API from the browser. By the end you understand when to use cookie authentication and when to use tokens, what core provides versus what you add from contrib, and you have CORS configured for your front end’s origin.
If your client only ever reads content the anonymous role can already view, you need neither authentication nor (for a same-origin server-side fetch) CORS. Reach for this page when the client is a browser app on another origin, or when it must read or write content beyond anonymous access.
Prerequisites
Section titled “Prerequisites”- A working read flow from JSON:API basics.
- Write access to
sites/default/services.yml(orsites/default/default.services.ymlto start from the shipped defaults). - An understanding of access: users, roles, and permissions; authentication only identifies the account, access decides what it may do.
Pick an authentication method
Section titled “Pick an authentication method”Core ships two authentication providers usable by an API client:
- Cookie authentication is the provider behind the normal login form, the
user.authentication.cookieservice. The browser sends the session cookie automatically. It is the right choice when the front end shares a parent domain with Drupal (so the cookie is in scope) and you can carry the CSRF token that writes require. Cross-origin cookie auth needssupportsCredentialsin CORS and a cookie configured to cross origins. - Basic authentication comes from the
basic_authcore module (drush pm:install basic_auth), thebasic_authprovider. It sends the username and password on every request. It is fine for server-to-server scripts and local testing over HTTPS, and wrong for a public browser client, which would have to hold a password.
Both plug into the authentication system through \Drupal\Core\Authentication\AuthenticationProviderInterface. A request with no recognized credentials runs as the anonymous user; authentication never grants access on its own, it only sets which account the access checks then run against.
Token authentication and the OAuth landscape
Section titled “Token authentication and the OAuth landscape”For a public single-page or native app, neither shipping a password nor relying on a cross-site cookie is appropriate. The standard answer is OAuth 2 bearer tokens, and that capability lives in contrib (community-contributed modules you install separately), not in core:
- Simple OAuth (
drupal/simple_oauth) adds an OAuth 2 server: it issues access and refresh tokens and registers a bearer-token authentication provider that JSON:API requests then use with anAuthorization: Bearer <token>header. - Consumers (
drupal/consumers) models each client application as a consumer entity. Simple OAuth depends on it to scope tokens to a specific client.
Core does not ship either module and does not issue tokens itself. State this plainly to readers evaluating the stack: the JSON:API endpoint is core, the OAuth token server is a contrib addition you install with Composer (PHP’s dependency manager, the equivalent of npm: composer require drupal/simple_oauth). Once installed, the client exchanges credentials for a token at the module’s token endpoint and sends that token on every API request; Drupal authenticates the request and runs the same access checks as any other.
Understand CORS and why it is off
Section titled “Understand CORS and why it is off”CORS (Cross-Origin Resource Sharing) is a browser policy. When JavaScript on http://localhost:3000 calls an API on https://my-site.ddev.site, the browser blocks reading the response unless the API returns headers permitting that origin. A server-side fetch (Node, curl) is not a browser and is not subject to CORS, which is why the Next.js quickstart fetches from server components without touching this setting.
Drupal ships CORS disabled. In default.services.yml the cors.config block sets enabled: false, and the http_middleware.cors service applies that config (%cors.config%) on every response. While it is off, Drupal sends no Access-Control-* headers and cross-origin browser requests fail in the browser, regardless of authentication.
Enable CORS in services.yml
Section titled “Enable CORS in services.yml”Copy sites/default/default.services.yml to sites/default/services.yml if you have not already, then set the cors.config block. List your front end’s exact origin rather than *:
parameters: cors.config: enabled: true # Request headers the browser may send. The CSRF token header is needed # for cookie-authenticated writes, Authorization for Basic/Bearer auth. allowedHeaders: ['x-csrf-token', 'authorization', 'content-type', 'accept'] allowedMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'] # List your front end's exact origin. Never ship '*' together with # supportsCredentials: true: Drupal then echoes back any request's # Origin, so every website gets credentialed access. allowedOrigins: ['http://localhost:3000'] allowedOriginsPatterns: [] exposedHeaders: false # Cache preflight responses for 10 minutes. maxAge: 600 # Required for cookie-based authentication across origins. supportsCredentials: trueRebuild the cache after editing the file: drush cache:rebuild. The settings map to response headers one for one: allowedOrigins to Access-Control-Allow-Origin, allowedMethods to Access-Control-Allow-Methods, allowedHeaders to Access-Control-Allow-Headers, and maxAge to how long the browser caches the preflight OPTIONS response.
Combine CORS with credentials safely
Section titled “Combine CORS with credentials safely”Two pairings matter when the client sends credentials (a cookie or an Authorization header):
- Set
supportsCredentials: trueso Drupal returnsAccess-Control-Allow-Credentials: true. Without it the browser discards an authenticated cross-origin response. - Never combine
supportsCredentials: truewithallowedOrigins: ['*']. Drupal does not answer that combination with the*wildcard: it echoes back whateverOriginthe request carries, so every website on the internet passes the check. With a session cookie configured to cross origins, any page a logged-in user visits can then read their authenticated responses. List explicit origins, or anallowedOriginsPatternsregular expression anchored to domains you control.
Include x-csrf-token in allowedHeaders if the client uses cookie authentication for writes, and authorization if it sends bearer or basic credentials.
Verify
Section titled “Verify”A preflight request to a JSON:API route returns the allow headers for your origin:
curl -i -X OPTIONS https://my-site.ddev.site/jsonapi/node/article \ -H 'Origin: http://localhost:3000' \ -H 'Access-Control-Request-Method: GET'The response includes Access-Control-Allow-Origin: http://localhost:3000 and Access-Control-Allow-Credentials: true. If those headers are absent, the cache was not rebuilt or enabled is still false. To check cookie-authenticated writes, log in over JSON to store the session cookie in a file, then fetch a CSRF token with that cookie:
curl -c jar -H 'Content-Type: application/json' \ -d '{"name":"editor","pass":"your-password"}' \ 'https://my-site.ddev.site/user/login?_format=json'curl -b jar https://my-site.ddev.site/session/tokenThe second command prints the token string to put in the X-CSRF-Token header on writes, sent together with -b jar.
Variations
Section titled “Variations”- Same-origin deployment. Serving the front end and Drupal from one origin (a reverse proxy, a subpath) avoids CORS entirely and lets cookie authentication work without
supportsCredentials. This is the simplest secure setup when the architecture allows it. - Server-side rendering only. A framework that fetches on the server (Next.js server components, SvelteKit load functions) never triggers CORS, because the request does not originate in a browser. You still authenticate, but you skip the CORS block.
- Per-origin patterns. Use
allowedOriginsPatternswith a regular expression when preview deployments produce many origins under one domain, instead of listing each.
Related
Section titled “Related”- JSON:API basics and JSON:API relationships and writes are what these credentials give access to.
- Next.js on Drupal quickstart reads from JSON:API server-side, so it needs neither setting at first.
- Access: users, roles, and permissions is where an authenticated request’s permissions are decided.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.