@logi-auth/server
v2.0.0
Published
Server-side "Sign in with logi" for Node backends (Next.js, Express, Fastify) — confidential OAuth 2.0 code exchange + id_token (RS256) verification. Zero runtime dependencies.
Maintainers
Readme
@logi-auth/server
Server-side "Sign in with logi" for Node backends — confidential OAuth 2.0 Authorization Code exchange + id_token (RS256) verification. Zero runtime dependencies (uses the Node global WebCrypto). Works in Next.js route handlers / server actions, Express, Fastify, or any Node server.
This is the confidential / backend counterpart to the public-client SDKs
(@logi-auth/browser, iOS, Android, Flutter). If your RP has a backend, verify
on the server with this library — do not rely on a client-side check.
Why it matters: a backend that skips the id_token
audcheck can be tricked into accepting a token minted for a different client (cross-client account takeover).exchangeCodeAndVerify()always verifies signature + iss + aud + exp + nonce before returningsub.
Supported versions
| Requirement | Version |
|-------------|---------|
| Node.js | >= 20 (global fetch + crypto.subtle; on Node 18 pass your own fetch) |
| Next.js | >= 13.4 (App Router — route handlers / server actions); Pages API routes also fine |
| Express / Fastify | any current version |
| TypeScript | >= 5.0 (types shipped; JS consumers fine too) |
Install
npm i @logi-auth/serverNext.js (App Router) example
// app/api/auth/logi/route.ts (start the flow)
import { LogiAuthServer } from "@logi-auth/server";
import { cookies } from "next/headers";
import { randomBytes, createHash } from "node:crypto";
const logi = new LogiAuthServer({
clientId: process.env.LOGI_CLIENT_ID!,
clientSecret: process.env.LOGI_CLIENT_SECRET!, // confidential client
redirectUri: "https://app.example.com/api/auth/logi/callback",
});
export async function GET() {
const state = randomBytes(16).toString("hex");
const nonce = randomBytes(16).toString("hex");
// PKCE is required by logi for every client type, confidential included.
const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const jar = await cookies();
jar.set("logi_state", state, { httpOnly: true, secure: true, sameSite: "lax" });
jar.set("logi_nonce", nonce, { httpOnly: true, secure: true, sameSite: "lax" });
jar.set("logi_verifier", codeVerifier, { httpOnly: true, secure: true, sameSite: "lax" });
return Response.redirect(logi.authorizationUrl({ state, nonce, codeChallenge }));
}// app/api/auth/logi/callback/route.ts (finish + verify)
export async function GET(req: Request) {
const url = new URL(req.url);
const jar = await cookies();
if (url.searchParams.get("state") !== jar.get("logi_state")?.value) {
return new Response("state mismatch", { status: 400 });
}
const session = await logi.exchangeCodeAndVerify({
code: url.searchParams.get("code")!,
nonce: jar.get("logi_nonce")!.value,
codeVerifier: jar.get("logi_verifier")!.value,
});
// session.sub is the verified pairwise subject — key your user record on it.
// ...set your own session cookie here...
return Response.redirect("https://app.example.com/");
}Identity verification receipts
When you ask a user to approve something outside of login — a payment, an
account change — logi returns the decision as a signed receipt (JWS). The
polling response's status is a claim you have to trust logi for; the receipt
verifies against the public JWKS on its own, so you can store it and show it to
a third party later. Full API: Identity Verification guide.
import { verifyVerificationReceipt } from "@logi-auth/server";
// jwks: fetched from https://api.1pass.dev/.well-known/jwks.json (cache it)
const decision = await verifyVerificationReceipt(receipt, {
jwks,
expected: {
issuer: "https://api.1pass.dev",
clientId: process.env.LOGI_CLIENT_ID!,
nonce, // the nonce you sent when creating the request
bindingDigest, // the action you are about to execute
},
});
// decision.sub / .authReqId / .purpose / .decidedAt — safe to act on.Verifying the signature alone is not enough: a valid signature is also carried
by another RP's receipt, another request's receipt, a stale receipt, and a
receipt for a different action. verifyVerificationReceipt checks typ, iss,
aud, exp, nonce, decision, and binding_digest together, and throws
ReceiptError with a code naming which one failed.
Pass expected.decision: "denied" to verify a decline instead — the default is
"approved", since the usual call site is "may I execute this now?".
Public client (no secret)
Omit clientSecret for a public client. PKCE (codeChallenge / codeVerifier)
is required either way — see the confidential example above — so pass it here
too:
const logi = new LogiAuthServer({ clientId, redirectUri }); // no secret
const url = logi.authorizationUrl({ state, nonce, codeChallenge });
const session = await logi.exchangeCodeAndVerify({ code, nonce, codeVerifier });License
MIT
