<!-- https://docs.elchi.dev/sdk | EAuth | Elchi Docs -->

# JavaScript SDK

Four packages. The core does the protocol and depends on nothing; the
others only turn its state into the framework's.

| Package | For |
|---|---|
| `@elchi-studios/eauth` | any web page |
| `@elchi-studios/eauth-react` | React 18 and 19, Next.js |
| `@elchi-studios/eauth-sveltekit` | Svelte 4 and 5, SvelteKit |
| `@elchi-studios/eauth-nuxt` | Nuxt 3 and 4 |

Create the application in the console as a **public** client and register
the redirect URI exactly as you pass it. There is no secret to pass: a
browser cannot keep one.

An access token lives 15 minutes. The SDK asks for `offline_access` so it
can renew the token before it runs out. New applications may have it; for
one whose settings leave it out, EAuth leaves it out of the sign-in, and the
session ends with the access token.

## React

```bash
npm install @elchi-studios/eauth-react
```

```tsx
import {
  EAuthProvider, SignedIn, SignedOut, SignInButton, useEAuth,
} from "@elchi-studios/eauth-react";

export default function App() {
  return (
    <EAuthProvider clientId="eauth_pub_..." redirectUri={location.origin + "/"}>
      <SignedOut><SignInButton /></SignedOut>
      <SignedIn><Profile /></SignedIn>
    </EAuthProvider>
  );
}

function Profile() {
  const { user, signOut } = useEAuth();
  return (
    <>
      <p>Hello {user?.name}</p>
      <button onClick={() => signOut()}>Sign out</button>
    </>
  );
}
```

The provider completes the sign-in when EAuth sends the browser back,
continues the session after a reload, and returns the person to the page
they started on.

## Svelte and SvelteKit

```bash
npm install @elchi-studios/eauth-sveltekit
```

```ts
// src/lib/auth.ts
import { goto } from "$app/navigation";
import { createAuth } from "@elchi-studios/eauth-sveltekit";

export const auth = createAuth({
  clientId: "eauth_pub_...",
  redirectUri: "https://app.example/",
  onReturn: (path) => goto(path, { replaceState: true }),
});
```

`$auth` is `{ user, loading, error }`, and `auth.signIn()`, `auth.signOut()`
and `auth.fetch()` do what they say. Nothing touches a browser API during
server rendering.

## Nuxt

```bash
npm install @elchi-studios/eauth-nuxt
```

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@elchi-studios/eauth-nuxt"],
  eauth: { clientId: "eauth_pub_..." },
});
```

`useEAuth()` is auto-imported, `definePageMeta({ middleware: "eauth" })`
requires somebody signed in, and every option can be set per environment
with `NUXT_PUBLIC_EAUTH_*`. Without `redirectUri` the application's root is
used.

## Without a framework

```bash
npm install @elchi-studios/eauth
```

```js
import { EAuth } from "@elchi-studios/eauth";

const auth = new EAuth({
  clientId: "eauth_pub_...",
  redirectUri: location.origin + "/",
});

// On every page load. Completes a sign-in when EAuth has just sent the
// browser back, and otherwise continues the session.
const { user, returnTo } = await auth.ready();
if (returnTo) history.replaceState(null, "", returnTo);

if (!user) signInButton.onclick = () => auth.signIn();
```

## Calling your own API

```js
const response = await auth.fetch("/api/orders");
```

`fetch` attaches the access token and renews it before it expires;
`getAccessToken()` gives the token itself for another client. Your server
verifies it as a signed JWT against `https://eauth.me/.well-known/jwks.json` and
checks `iss`, `aud` (your client ID) and `exp`.

## Organisations

For an application with [organisations](/organisations) turned on, the user
carries the one the person signed in for, and a sign-in can name one by its
ID or slug, which is also how somebody switches:

```js
user.organization; // { id, slug, name, role, permissions } or undefined
await auth.signIn({ organization: "acme-ag" });
```

The same option works on the React, Svelte and Nuxt packages' `signIn`.

## What the SDK does that a tutorial would not

**No client secret anywhere.** If you want to pass one, the client is
registered as the wrong type.

**Tokens in memory by default.** A token in `localStorage` is readable by any
script on the page, so one cross-site scripting bug in any dependency hands
over the session. After a reload the SDK asks EAuth again with
`prompt=none`: one redirect that shows nothing. `storage: "local"` keeps the
refresh token across reloads for those who have weighed it.

**`state`, `nonce`, `iss`, `aud` and `azp` are all verified**, and a mismatch
throws rather than warns. The `iss` check is RFC 9207 and is what stops a
mix-up attack when your application talks to more than one provider.

**Refreshes never overlap.** Refresh tokens rotate, and a rotated token
presented again ends the session. Callers in a page share one refresh, and
with `storage: "local"` the tabs take turns.

**No redirect loops.** A failed sign-in is shown, not retried; a silent
attempt that failed is not repeated; and an answer that does not arrive at
the redirect URI is reported as `redirect_mismatch` instead of tried again.

## Requests from the browser

The SDK calls four of EAuth's addresses with `fetch` from your page, and
EAuth answers them with CORS headers, as narrowly as each allows:

| Address | Pages that may read the answer |
|---|---|
| `/.well-known/openid-configuration`, `/.well-known/jwks.json` | any |
| `/userinfo` | any, with the access token in the `Authorization` header |
| `/token`, `/revoke` | pages on the origin of one of the application's redirect URIs |

A page on `https://app.example.ch` can exchange its code and renew its
tokens once a redirect URI on `https://app.example.ch` is registered. From
any other origin the browser hides the answer, and the SDK can only report
that EAuth could not be reached. Register a redirect URI on every origin the
application runs on, `http://localhost:5173/` for development included.

No address takes a cookie from another origin, so a request never needs
`credentials: "include"`, and one that sets it is refused by the browser.
The sign-in pages, the account page and everything else that works with
EAuth's own session cannot be read from another origin at all.

## Errors

Everything throws `EAuthError` with a `code` you can branch on.

| Code | Meaning |
|---|---|
| `issuer_mismatch` | The answer or a token came from a different server |
| `audience_mismatch` | The ID token was issued for a different application |
| `nonce_mismatch` | The ID token belongs to a different sign-in |
| `token_expired` | The ID token has expired; usually the device clock is wrong |
| `redirect_mismatch` | EAuth's answer did not arrive at the redirect URI |
| `sign_in_loop` | Three sign-ins in a row failed, so no fourth was started |
| `organization_mismatch` | The ID token is for another organisation than the one named |
| `access_denied` | The person declined, or is not a member of the organisation named |
| `invalid_client` | The client ID is wrong, or the application is confidential: code in a browser needs a public one |

## Self-hosting

Pass `issuer` pointing at your own deployment. Everything else is discovered
from `/.well-known/openid-configuration`, so no other URL is hardcoded.
