Server SDK

@loginwith/client — verify tokens server-side, any language.

The server SDK is a thin wrapper around JWT verification + the LoginWith REST API. It’s where your backend confirms the bearer token came from LoginWith, figures out who’s calling, and makes authenticated API calls on their behalf.

Install

npm install @loginwith/client

Construct

import { LoginWith } from "@loginwith/client"

const lgw = new LoginWith({
  client_id: process.env.LGW_CLIENT_ID!,
  client_secret: process.env.LGW_CLIENT_SECRET!,
})

The SDK auto-discovers your issuer and audience from the client_id (both are derived from your org slug on the server side — you don’t need to pass either).

Verify a bearer token

const [caller, error] = await lgw.verify(req.headers.authorization)

if (error) {
  // Invalid token, expired, wrong audience, missing kid, etc.
  return res.status(401).end()
}

// caller.userId  — present on user tokens
// caller.clientId — always set (which client minted the token)
// caller.orgId   — the owning org
// caller.scopes  — optional array of scopes granted

lgw.verify does all the JWKS lifting (fetch, cache, rotate on kid miss), signature validation (RS256), and standard claim checks (iss, aud, exp, nbf) in one call.

Framework middleware

Rather than calling verify yourself in every handler, drop in the adapter for your framework:

  • @loginwith/authenticate-koa
  • @loginwith/authenticate-express
  • @loginwith/authenticate-hono
  • @loginwith/authenticate-nest
  • @loginwith/authenticate-fastify
  • @loginwith/authenticate-elysia
  • @loginwith/authenticate-flask, authenticate-fastapi, authenticate-django
  • @loginwith/authenticate-rails, authenticate-laravel, authenticate-phoenix
  • @loginwith/authenticate-ktor, authenticate-axum
  • @loginwith/authenticate-node (bare node:http)

Each exposes an authenticate(lgw, { whitelist }) function that wraps the app, populates ctx.state.caller (or the framework’s equivalent), and short-circuits unauthenticated requests with a 401.

Resource namespaces

Once you have an lgw instance, the REST API is exposed via typed methods:

await lgw.users.me()                  // the authenticated caller (via async ctx)
await lgw.users.get(userId)           // fetch by id
await lgw.users.list({ cursor, limit })

await lgw.orgs.get(orgId)
await lgw.tenants.list({ orgId })

All methods are scoped to your client_id’s org unless the client is the LoginWith console client (cross-org admin surface).