@cnvx/sign-in
v0.1.0
Published
Sign-in with OpenID Connect for a site's own server: a session cookie, routes for any Fetch API server, a SvelteKit hook, and an account button.
Maintainers
Readme
@cnvx/sign-in
Sign-in with OpenID Connect for a site's own server, e.g. against id.convex.works (Pocket ID). The session is an encrypted, HttpOnly cookie on the site's origin: the provider's tokens stay in it, and the browser's scripts see only who is signed in.
- The code flow with PKCE, state and nonce; ID tokens are checked by openid-client.
- The session renews with the provider when its tokens expire (about hourly
with Pocket ID). Someone the provider disables, or that
allowno longer admits, is signed out then. - Signing out also ends the session with the provider.
- Without a provider, a development sign-in with any name, on localhost only.
npm install @cnvx/sign-inServer
import { createSignIn } from '@cnvx/sign-in/server';
const signIn = createSignIn({
issuer: 'https://id.convex.works',
clientId: env.OIDC_CLIENT_ID,
clientSecret: env.OIDC_CLIENT_SECRET,
secret: env.SESSION_SECRET, // 32+ random characters, the same on every instance
scope: 'openid profile email groups offline_access',
allow: (user) => user.groups?.includes('team') || user.email?.endsWith('@example.com'),
devLogin: dev // only used when the provider is not configured
});SvelteKit:
// src/hooks.server.ts
export const handle = signIn.sveltekit(); // sets event.locals.user// src/app.d.ts
declare global {
namespace App {
interface Locals { user: import('@cnvx/sign-in').User | null }
}
}Any server with the Fetch API (Node, Bun, Deno, Workers):
const app = signIn.wrap(async (request, user) => new Response(`Hello ${user?.name ?? 'stranger'}`));signIn.handle(request) answers the routes and returns undefined for other
requests; signIn.check(request) gives the user and the cookies to send when
the session was renewed or ended.
| Option | Meaning |
|---|---|
| issuer, clientId, clientSecret | A confidential client with the code flow and S256 PKCE. |
| secret | Encrypts the session cookie. At least 32 characters. |
| path | Where the routes are. Default /auth. |
| callback | The redirect address registered with the provider, as a path. Default <path>/callback. It may be a page of the site, such as its base path. |
| home | Where people go after signing out. Register <origin><home> as a logout redirect. Default /. |
| origin | The site's origin, when the server sees another one behind a proxy. |
| scope | Default openid profile email offline_access. offline_access gives the refresh token; groups the provider's groups. |
| allow | Who may sign in, from { sub, name, email, picture, groups }. Checked at sign-in and at every renewal. An address the provider has not verified is left out. |
| maxAge | How long a session lasts at most, in seconds. Default seven days. |
| cookie | The cookie's name. Default session. |
| devLogin | Without a provider: sign in with any name, on localhost only. Never with NODE_ENV=production. Sessions end when the server restarts unless secret is set. |
| devHosts | More host names the development sign-in works on, e.g. the machine's address on a private network (['100.64.0.2']). Whoever reaches the site there can sign in as anyone. |
Routes, under path:
| Route | |
|---|---|
| GET /login?returnTo=/page | Leaves for the provider, and comes back to returnTo (a path on the site). |
| GET <callback> | The provider's redirect. Errors come back as ?auth_error=cancelled, not_allowed or signin_failed. |
| POST /logout | From a form on the site. Ends the session, then with the provider. |
| GET /session | { user, configured, error, expiresAt } for the browser. |
Browser
<script lang="ts">
import { Account, SignInSession } from '@cnvx/sign-in';
let { data } = $props();
const session = new SignInSession({ user: data.user }); // the user the server rendered with
$effect(() => session.start()); // keeps it current, shows ?auth_error once
</script>
<Account {session} />Account is the avatar with a menu to sign out, or Sign in; it takes the
surrounding font and colour. Without Svelte, link to
/auth/login?returnTo=… and sign out with a form that POSTs to /auth/logout
(signInUrl() and signOut() do that).
License
Development
pnpm install
pnpm test # builds, then runs the flows against a fake provider
pnpm check