Skip to content

Authentication endpoints & headers

Lookup material for authenticating against a site’s API. For instruction, see the auth quickstart (first token in 10 minutes) and the auth guide (grant choice, scoping, storage, rotation). Both endpoints are also described in the machine-readable OpenAPI 3.1 spec under the Authentication tag, published at a stable URL for code generators and API collections.

All paths are relative to the site base URL (DRUPAL_SITE_URL, no trailing slash). Credentials are an API client’s ID and secret, created in your site’s admin UI at API > API clients. These endpoints exist on Source CMS sites. A headless site on Cloud Platform authenticates against your own Drupal application’s OAuth setup, which you configure and operate yourself.

How the two endpoints cooperate in the authorization_code grant; the other two grants skip /oauth/authorize and call POST /oauth/token directly:

Starts the authorization_code grant: the user’s browser is redirected here, the user logs in and grants permissions, and the site redirects back with a temporary code. Not used by the other grants.

Query parameters:

Parameter Type Required Default Description
response_type string yes none The literal value code
client_id string yes none The API client’s ID
redirect_uri string yes none Must match one of the client’s configured Redirect URIs
scope string no every scope selected on the client Space-separated subset of the client’s selected scopes

Response: a redirect to redirect_uri carrying the temporary authorization code, which is then exchanged at POST /oauth/token with grant_type=authorization_code.

Used in: auth guide, step 2.

Issues access tokens for all three grant types. Request body is form-encoded:

Content-Type: application/x-www-form-urlencoded

Request parameters per grant type:

Server-to-server; access is based on the client’s permissions, not a user’s.

Parameter Type Required Default Description
grant_type string yes none The literal value client_credentials
client_id string yes none The API client’s ID
client_secret string yes none The API client’s secret
scope string no every scope selected on the client Space-separated subset of the client’s selected scopes

Example exchange (token redacted):

Terminal window
curl -s -X POST "$DRUPAL_SITE_URL/oauth/token" \
-d "grant_type=client_credentials" \
-d "client_id=$DRUPAL_CLIENT_ID" \
-d "client_secret=$DRUPAL_CLIENT_SECRET"
{
"token_type": "Bearer",
"expires_in": 300,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUz…(redacted)"
}

Used in: auth quickstart, auth guide, step 2.

Exchanges the code from GET /oauth/authorize for a token. All parameters are required strings with no defaults:

Parameter Description
grant_type The literal value authorization_code
code The authorization code from the redirect
client_id The API client’s ID
client_secret The API client’s secret
redirect_uri The same redirect URI used at /oauth/authorize

Used in: auth guide, step 2.

Exchanges a refresh token for a new access token, without user interaction. All parameters are required strings with no defaults:

Parameter Description
grant_type The literal value refresh_token
refresh_token The refresh token
client_id The API client’s ID
client_secret The API client’s secret

Used in: auth guide, step 2.

Success is 200 with JSON:

{
"token_type": "Bearer",
"expires_in": 300,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI…(redacted)"
}
Field Type Meaning
token_type string Always Bearer, use the header format below
expires_in integer Access token lifetime in seconds, counted from issuance
access_token string The token itself
refresh_token string Returned by the authorization_code and refresh_token grants. Never returned by client_credentials (absent from the captured response above, and confirmed in the OAuth server implementation): server-to-server clients re-request tokens with their credentials instead

Failure is 401 with a JSON error body; see Error codes.

Every authenticated API request carries the access token in this exact header:

Authorization: Bearer <token>

One space between Bearer and the token. Canonical example against the JSON:API root:

Terminal window
curl -s "$DRUPAL_SITE_URL/api" \
-H "Accept: application/vnd.api+json" \
-H "Authorization: Bearer $TOKEN"

Used in: auth quickstart, step 3, every request in the Content API reference.

Scopes are selected as checkboxes on the API client (Allowed Scopes) and are the token’s defaults:

  • A token request without a scope parameter carries every selected scope.
  • A scope parameter may narrow to a subset of them.
  • Naming an unknown or unselected scope returns the invalid_scope error.

Scopes gate writes and admin surfaces, not published-content reads: a GET for published content succeeds with any valid token regardless of scope, and with no token at all when the site’s Public access (API > JSON:API) is Yes. Reading unpublished content requires content:administer.

The full vocabulary:

Scope Governs Used in
content:administer Content CRUD over the JSON:API: create, update, and delete entries. There is no read-only content scope. Auth quickstart, writing content guide
content_type:administer Content types (bundles) N/A
content_type:administer_fields Fields on content types N/A
page_template:administer Page templates N/A
media:administer Media entries N/A
media_type:administer Media types N/A
taxonomy:administer Taxonomy vocabularies and terms N/A
taxonomy:administer_fields Fields on taxonomy terms N/A
menu:administer Menus N/A
site_settings:administer Site settings N/A
canvas:page:read Read Canvas pages N/A
canvas:page:create Create Canvas pages N/A
canvas:page:edit Edit Canvas pages N/A
canvas:page:delete Delete Canvas pages N/A
canvas:page_region Canvas page regions N/A
canvas:content_template Canvas content templates N/A
canvas:asset_library Drupal Canvas CLI (@drupal-canvas/cli): asset library access for downloading and uploading components Canvas components
canvas:js_component Drupal Canvas CLI (@drupal-canvas/cli): JS component access for downloading and uploading components Canvas components
canvas:brand_kit Canvas brand kits N/A
canvas:media:view View media from Canvas N/A
canvas:media:image:create Create images from Canvas N/A
member Basic member access N/A

This table matches the auth guide’s troubleshooting, which adds context per entry.

Error Cause Fix
{"error":"invalid_client","error_description":"Client authentication failed"}, 401 from /oauth/token The client ID or secret sent to /oauth/token is wrong. Re-copy both from API > API clients; if the secret is lost, create a new client. The secret is shown only once.
401 Unauthorized, on an API request that worked before; the explanation is in the WWW-Authenticate response header (error="access_denied", error_description="Access token could not be verified"), not the body The access token outlived its expires_in lifetime: 300 seconds (5 minutes). Request a fresh token from /oauth/token and retry; have your code re-request tokens automatically rather than caching one forever.
401 Unauthorized, on an API request that has never worked. The body is HTML, not JSON; the diagnostic is only in the WWW-Authenticate response header, e.g. (captured) Bearer realm="OAuth", error="access_denied", error_description="The JWT string must have two dots" for a malformed token The Authorization header is missing, malformed, or carries a token this site did not issue. Send exactly Authorization: Bearer <token> (one space after Bearer) with a token issued by the same site you’re calling; read the header with curl -i.
{"error":"invalid_scope","error_description":"The requested scope is invalid, unknown, or malformed","hint":"Check the `content:read` scope"}, 400 from /oauth/token; the hint names the offending scope The token request’s scope parameter names a scope that doesn’t exist or isn’t selected on the client. Omit the scope parameter (the token then carries every scope selected on the client), or name only selected scopes from the scope table.
403 Forbidden, JSON:API error document whose detail names the missing permission (e.g. The 'administer nodes' permission is required.) The token is valid but doesn’t carry the scope the request needs, for example, writing content with a token from a client that doesn’t have content:administer selected. Select the scope on the API client at API > API clients, then request a new token. Existing tokens don’t gain scopes retroactively.
…blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource., browser console The site’s allowed origins have been restricted at API > CORS configuration and the browser origin making the request isn’t one of them (CORS is open by default). Add the origin under Allowed origins at API > CORS configuration, or move the call server-side, where CORS doesn’t apply.
Token Lifetime
Access token 300 seconds (5 minutes), reported per token as expires_in.
Authorization code 300 seconds: the code from GET /oauth/authorize must be exchanged within 5 minutes (product configuration, confirmed 2026-07-03).
Refresh token 1209600 seconds (14 days; product configuration, confirmed 2026-07-03).
  • Auth quickstart: obtains a client_credentials token and makes the first authenticated call.
  • Auth guide: chooses between these grant types and covers scoping, storage, and rotation.
  • Content API reference: the endpoints these tokens authorize.

Was this page helpful?