Skip to content

About

Server-side Sign in with logi for Node backends (Next.js, Express) — confidential OAuth 2.0 + id_token (RS256) verification

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

@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 aud check 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 returning sub.

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/server

Next.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

About

Server-side Sign in with logi for Node backends (Next.js, Express) — confidential OAuth 2.0 + id_token (RS256) verification

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages