Some JSON APIs issue a JSON Web Token JWT from a login or refresh endpoint and expect cURL to reuse that token on later requests. This is the right pattern when automation needs a short-lived API credential instead of sending the account password to every protected call.
In cURL, the clean path is to request the token with --json, extract the returned access_token field, and send the follow-up request with --oauth2-bearer. That keeps the JSON payload, auth header, and protected request flow aligned with how current token endpoints usually behave.
The operational risk is still secret handling, not request syntax. Keep token responses out of shared logs, avoid bearer-token calls that follow unexpected redirects, and clear shell variables when the request batch ends. The examples below use masked service-account values and base64url-shaped JWT segments so the request and response shape stays realistic without publishing reusable credentials.
Steps to authenticate with JSON Web Tokens in cURL:
- Define the token endpoint and request a JWT with a JSON body.
$ AUTH_URL='https://api.example.net/jwt/token' $ AUTH_RESPONSE="$(curl --silent --show-error \ --json '{"username":"svc-metrics-reader","password":"replace-with-issued-secret"}' \ "$AUTH_URL")" $ printf '%s\n' "$AUTH_RESPONSE" | jq . { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdmMtbWV0cmljcy1yZWFkZXIiLCJzY29wZSI6Im1ldHJpY3M6cmVhZCIsImF1ZCI6ImFwaS5leGFtcGxlLm5ldCJ9.c2lnbmF0dXJlLXJlZGFjdGVk", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdmMtbWV0cmljcy1yZWFkZXIiLCJ0eXAiOiJyZWZyZXNoIiwiYXVkIjoiYXBpLmV4YW1wbGUubmV0In0.cmVmcmVzaC1zaWduYXR1cmUtcmVkYWN0ZWQ" }--json is shorthand for sending the body as JSON while also adding Content-Type: application/json and Accept: application/json to the request.
- Extract the access token and related fields into shell variables before reusing them.
$ ACCESS_TOKEN="$(printf '%s\n' "$AUTH_RESPONSE" | jq -r '.access_token')" $ REFRESH_TOKEN="$(printf '%s\n' "$AUTH_RESPONSE" | jq -r '.refresh_token')" $ EXPIRES_IN="$(printf '%s\n' "$AUTH_RESPONSE" | jq -r '.expires_in')" $ printf 'token bytes=%s\nexpires_in=%s\n' "${#ACCESS_TOKEN}" "$EXPIRES_IN" token bytes=162 expires_in=3600Do not print the full token into shared terminals, copied support logs, or CI output.
The extraction examples use jq for field selection; replace it with an equivalent JSON parser if your environment uses another tool.
- Send the issued JWT to the protected API and confirm that the request is authenticated.
$ API_URL='https://api.example.net/jwt/protected' $ curl --silent --show-error \ --oauth2-bearer "$ACCESS_TOKEN" \ --write-out '\nHTTP %{http_code}\n' \ "$API_URL" {"authenticated": true, "subject": "svc-metrics-reader", "scope": "metrics:read"} HTTP 200--oauth2-bearer is cleaner than composing the Authorization: Bearer header manually for a standard bearer-token request.
- Inspect the exact Authorization header when the API says the token was missing or malformed.
$ HEADER_URL='https://api.example.net/jwt/headers' $ curl --silent --show-error \ --oauth2-bearer "$ACCESS_TOKEN" \ "$HEADER_URL" | jq -r '.authorization' Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdmMtbWV0cmljcy1yZWFkZXIiLCJzY29wZSI6Im1ldHJpY3M6cmVhZCIsImF1ZCI6ImFwaS5leGFtcGxlLm5ldCJ9.c2lnbmF0dXJlLXJlZGFjdGVk
A header echo confirms that cURL sent the token in the expected Bearer format before you debug the application response.
- Check the failure path with an invalid token so the script does not treat an auth error as a valid empty response.
$ curl --silent --show-error \ --oauth2-bearer invalid-token \ --write-out '\nHTTP %{http_code}\n' \ "$API_URL" {"authenticated": false, "error": "invalid_token"} HTTP 401A deliberate 401 test is better proof of the auth boundary than assuming every non-empty JSON body means the login worked.
- Exchange the refresh token for a new access token and retry the protected call.
$ REFRESH_URL='https://api.example.net/jwt/refresh' $ REFRESH_RESPONSE="$(curl --silent --show-error \ --json "{\"refresh_token\":\"${REFRESH_TOKEN}\"}" \ "$REFRESH_URL")" $ ACCESS_TOKEN="$(printf '%s\n' "$REFRESH_RESPONSE" | jq -r '.access_token')" $ curl --silent --show-error \ --oauth2-bearer "$ACCESS_TOKEN" \ --write-out '\nHTTP %{http_code}\n' \ "$API_URL" {"authenticated": true, "subject": "svc-metrics-reader", "scope": "metrics:read"} HTTP 200Keep the refresh payload aligned with the API contract because some services use refreshToken or a grant object instead of refresh_token.
- Clear the JWT values from the current shell after the request batch finishes.
$ unset AUTH_RESPONSE REFRESH_RESPONSE ACCESS_TOKEN REFRESH_TOKEN EXPIRES_IN
Removing the variables reduces accidental reuse after the token should have expired or rotated.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
