# Authenticate requests

Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) hooks to attach an authentication header to the initial request and refresh credentials before a retry. The example below sends a bearer token to one trusted API origin and retries a rejected GET once with a fresh token.

## 1. Attach a token and refresh it on a 401

Install Ky in your project:

```bash
npm install ky
```

Run this module in the browser or Node.js with Fetch support. Replace `<access-token>` and `<refresh-token>` with your credentials, and change the API URLs to your service. This example expects `/auth/refresh` to accept a JSON refresh token and return the new access token as plain text. Adapt that request and body reader to your service's contract.

Use `beforeRequest` to set `request.headers`. It runs once before retry handling begins, not before every attempt. Use `beforeRetry` to update the header on a confirmed retry, and [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to distinguish an HTTP rejection from other failures.

```ts title="authenticated-request.ts"
import ky, {isHTTPError} from 'ky';

const trustedOrigin = 'https://api.example.com';
let accessToken = '<access-token>';
const refreshToken = '<refresh-token>';

function requireTrustedOrigin(request: Request): void {
	if (new URL(request.url).origin !== trustedOrigin) {
		throw new Error('Refusing to send credentials to an untrusted origin');
	}
}

const api = ky.extend({
	credentials: 'omit',
	redirect: 'error',
	retry: {
		limit: 1,
		methods: ['get'],
		statusCodes: [401],
	},
	hooks: {
		beforeRequest: [({request}) => {
			requireTrustedOrigin(request);
			request.headers.set('Authorization', `Bearer ${accessToken}`);
		}],
		beforeRetry: [async ({request, error}) => {
			requireTrustedOrigin(request);
			if (!isHTTPError(error) || error.response.status !== 401) {
				return;
			}

			accessToken = await ky.post(`${trustedOrigin}/auth/refresh`, {
				json: {refreshToken},
				credentials: 'omit',
				redirect: 'error',
				retry: 0,
			}).text();
			request.headers.set('Authorization', `Bearer ${accessToken}`);
		}],
	},
});

async function main(): Promise<void> {
	const response = await api.get(`${trustedOrigin}/users/me`);
	console.log(await response.text());
}

main().catch(console.error);
```

The refresh call uses `ky`, not `api`, so it does not inherit the authentication hooks. In your network inspector or server logs, look for an initial GET with the access token. If your service rejects it with 401, look for a refresh POST followed by a GET with the replacement token. The call returns the final response; the log contains your service's response body, not the refresh token. If authentication still fails, the request rejects after the single retry.

401 is not a default retry status. The explicit `statusCodes: [401]` makes it eligible here; `beforeRetry` does not itself decide whether to retry. See [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for broader retry policies.

## 2. Keep credentials within the intended origin

The example checks the actual `request.url` before adding or refreshing the header, keeps the retry at the same URL, and rejects redirects with `redirect: 'error'`. `credentials: 'omit'` disables Fetch-managed credentials; the bearer header is still set explicitly.

Replacement retry requests are used as-is and are not sanitized. Changing origins can forward credentials unintentionally. Before returning a `Request` from `beforeRetry`, or passing one to `ky.retry({request})`, remove every credential you do not want forwarded. For example, delete `Authorization` and any service-specific API-key headers from a copied `Headers` object and construct the replacement with `credentials: 'omit'`. Do not copy a sensitive request body to another origin either.

Returning a replacement `Request` from `beforeRetry` skips the remaining `beforeRetry` hooks. Sanitize it before returning it; do not rely on a later hook to strip credentials. Ky replaces its signal with the managed signal for timeout and abort handling, but does not strip its authentication headers.

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `hooks.beforeRequest` | Array of hook functions | `[]` | Changes the initial outgoing request; `retryCount` is always `0`. |
| `hooks.beforeRetry` | Array of hook functions | `[]` | Changes a confirmed retry; `retryCount` starts at `1`. |
| `retry.limit` | `number` | `2` | Bounds retries. The example allows one retry. |
| `retry.methods` | Readonly array of HTTP methods | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | Restricts automatic retries to eligible methods. |
| `retry.statusCodes` | `readonly number[]` | `[408, 413, 429, 500, 502, 503, 504]` | Selects eligible HTTP failures. The example selects 401. |

## Related

- [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains the hook stages, including response-driven retries with `afterResponse` and `ky.retry()`.
- [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) explains composing reusable clients with `extend()`.
- [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) explains inspecting the final rejection and reading its error data.
