Arrow keys to move, Enter to open, Escape to close.

  1. Docs
  2. EAuth
  3. JavaScript SDK

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

npm install @elchi-studios/eauth-react
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

npm install @elchi-studios/eauth-sveltekit
// 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

npm install @elchi-studios/eauth-nuxt
// 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

npm install @elchi-studios/eauth
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

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 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:

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.

6 min read