
# JavaScript browser SDK

`@keenauth/js` is the browser core of KeenAuth. It signs users in to your single-page app, keeps their tokens and calls your API. It has no runtime dependencies, and the React components build on it. The package is not published to npm yet.

## Sign in with a redirect

The SDK is a standard OpenID Connect public client:

- It uses PKCE (S256) and DPoP-bound tokens.
- It checks `state`, `nonce` and `iss` on the callback.
- It validates the ID token's signature and claims against your environment's keys.

```ts
import { KeenAuth } from "@keenauth/js";

const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  postLogoutRedirectUri: "https://app.acme.example/",
});

// On your sign-in button:
await auth.signInRedirect({ appState: "/orders" });

// On https://app.acme.example/callback:
const { appState } = await auth.handleRedirectCallback();
```

`handleRedirectCallback` accepts a response only once. It fails with a `KeenAuthError` when:

- `state` or `iss` does not match;
- the pending sign-in is older than 10 minutes;
- the ID token is not valid.

An error that the server sends back is raised as a `KeenAuthOAuthError` carrying the OAuth `error` code.

## Call your API

To get an access token for your API, pass its identifier as `resource` (RFC 8707). It is sent on the authorization request, the code exchange and every refresh, and the token's `aud` is that identifier:

```ts
const auth = new KeenAuth({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  redirectUri: "https://app.acme.example/callback",
  resource: "https://api.acme.example", // your API's identifier
  scope: "openid profile email offline_access messages:read",
});
```

- The value must be an absolute URI without a fragment (`#`). It is sent exactly as you write it, so use the API identifier as it is registered. An invalid value throws a `KeenAuthError` with code `config_invalid` from the constructor.
- `signInRedirect({ resource })` overrides it for one sign-in.
- Without `resource`, the server uses the application's default API, if one is set; otherwise no API scopes are granted.
- The option also accepts an array, sent as repeated `resource` parameters as RFC 8707 allows. A KeenAuth access token has exactly one audience, so the server rejects more than one value with `invalid_target` (a `KeenAuthOAuthError`).

```ts
const res = await auth.authorizedFetch("https://api.acme.example/v1/orders");

// Or take the token yourself; it is refreshed when it is about to expire.
const token = await auth.getAccessToken();
```

`authorizedFetch` sends `Authorization: DPoP` with a fresh proof bound to the token. If the server asks for a DPoP nonce, it retries once with that nonce. Concurrent calls share one refresh request, and every refresh rotates the refresh token. To verify these tokens in your API, see [Verifying tokens in your API](/docs/sdk/token-verification).

## Sign out

```ts
await auth.signOut();
```

`signOut` drops the tokens and revokes the refresh token. It then sends the browser to your environment's logout endpoint, which returns it to `postLogoutRedirectUri`.

## Where tokens are kept

- Access, refresh and ID tokens are kept only in memory, never in localStorage, sessionStorage, IndexedDB, cookies or the URL. After a page reload, the user is signed in again with a redirect; the SDK does not restore the session.
- sessionStorage holds only the one-time values of a sign-in in progress.
- The DPoP key is a non-extractable WebCrypto key, kept in memory by default.

## Embedded sign-in

The same object also gives the embedded components a front API client:

- `transactions` for sign-in steps;
- `me` for the account's profile and `user_metadata`, emails (with code verification), passkeys and sessions;
- `webauthn` for passkey ceremonies.

These calls use your environment's cookies and a CSRF header. The server accepts them only from the environment's own login origin.

## Sign in without a redirect

On a page outside your login origin, start the transaction with your `clientId`. The SDK then binds it to its DPoP key instead of cookies:

```ts
let tx = await auth.transactions.create({ client_id: "app_01j9...", organization: "acme" });
tx = await auth.transactions.step("identifier", { identifier: "ada@acme.example" });
tx = await auth.transactions.step("password", { password });
if (tx.state === "completed") await auth.complete(tx); // { kind: "signed_in" }
```

- The application must be a first-party application, and the page origin must be one of its allowed web origins.
- Every call carries a DPoP proof and no cookie. If the server asks for a DPoP nonce, the SDK retries once with that nonce.
- The SDK creates the PKCE verifier, `state` and `nonce` itself and keeps them in memory. `scope` and `resource` come from the options; you can pass `organization` and `uiLocales` to `create`. Use one `resource` value here.
- `complete` checks the answer, exchanges the authorization code with the same DPoP key and keeps the tokens in memory. The page does not navigate, and the code is never returned to your code.
- Every `step` call, with or without `client_id`, sends the browser's time zone in the `Idp-Zoneinfo` header. When the step completes the sign-in and the user has not picked a time zone, KeenAuth saves it to the profile's `zoneinfo`, as the hosted sign-in page does. If the browser reports no usable zone, the header is left out and the step is unchanged.
- If the sign-in needs a page the user must see (consent, legal terms, choosing an organization), the last step fails with a `KeenAuthProblemError` whose code is `authorization_failed`. Use `signInRedirect` for that user.

## Account calls from your own origin

Once the user is signed in and the SDK holds a refresh token (the default scope asks for `offline_access`), the `me` and `organizations` calls no longer need the sign-in host's session cookie:

- The SDK refreshes with `resource=<issuer>/fapi` and gets a short-lived access token for the front API. It is DPoP-bound and kept in memory only, like the other tokens.
- Each account call sends `Authorization: DPoP <token>` and a DPoP proof for that call, with no cookie. If the server asks for a DPoP nonce, the SDK retries once with that nonce; if the token is no longer valid, it gets a new one and retries once.
- The refresh token rotates on every refresh, so the SDK runs refreshes one after another.
- This works only for a first-party application whose allowed web origins include your page's origin.
- Without tokens, the SDK uses the session cookie as before.

See [Front API access token](/docs/api/fapi/front-api-token) for the server rules.

## Switching the organization

`organizations.switch(orgId)` moves the signed-in user to another organization without a page. Pass the `org_…` ID of an organization from `organizations.listMine()`:

```js
const { data } = await auth.organizations.listMine();
await auth.organizations.switch(data[1].id);
```

- It needs the same as the account calls above: a refresh token, `redirectUri` and a first-party application whose allowed web origins include your page's origin.
- The SDK sends a [switch request](/docs/api/fapi/transactions/create) with the front API token, exchanges the code with a proof of the same key and keeps the new tokens, whose `org_id` is the organization. The tokens you held stay until all of that succeeds.
- If the organization needs a page the user must see (MFA, SSO, consent), the call fails with a `KeenAuthProblemError` whose code is `authorization_failed`. Use `signInRedirect({ organization })` for that user.

## Current limits

- Profile writes (`me.update`, `me.updateMetadata`) send `If-Match` with the `ETag` of the last profile read or write. If the profile changed elsewhere in the meantime, the write fails with a `KeenAuthProblemError` whose code is `precondition_failed`; call `me.get()` again before retrying.
- Phone numbers are not available yet: `me.phones` is `undefined`.
