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.