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:

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:

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:

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",
});
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.

Sign out

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

Embedded sign-in

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

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:

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" }

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:

See Front API access 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():

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

Current limits