Pikbase Docs
Open console (opens the console)
Esc

Type to search.

Build

Authentication and tenancy

Exchange credentials for a token, verify it, and resolve tenant context server-side.

Contract sourcePikbase docs

Authentication proves identity. Authorization decides whether that identity may perform an operation on a tenant. They are separate steps and both run on the server.

Getting a token

Credentials are exchanged for a token at /api/auth/getToken. The TypeScript client wraps that call:

interface GetTokenRequest {
  email: string;
  password: string;
  remember?: boolean;
  includeUserInfo?: boolean;
  variation?: { tenantCode: string };
}

const response = await entityService.getToken({
  email,
  password,
  remember: true,
  includeUserInfo: true,
  variation: { tenantCode },
});

const token = response.auth?.token;

The response is a GsbAuthResponse:

Field Type Description
auth.token string Bearer token for subsequent requests
auth.userId string Identifier of the authenticated user
auth.tenantCode string Tenant the token was issued for
auth.roles string[] Role names granted to the user
auth.groups string[] Group names the user belongs to
auth.expireDate string Expiry timestamp
status number HTTP status of the exchange

The token encodes the user id (uid), tenant code (tc), instance id (i), expiry (exp), and issuer (iss).

This exchange runs on your server. The password never reaches a browser bundle, and the token never reaches localStorage.

Storing the session

Set the token in a cookie from a server route. HttpOnly keeps it out of reach of any script, so a single XSS cannot become account takeover.

// Server route. Never runs in the browser.
const auth = await entityService.getToken({
  email,
  password,
  variation: { tenantCode },
});

if (!auth.auth?.token) {
  return new Response("Invalid credentials", { status: 401 });
}

return new Response(null, {
  status: 204,
  headers: {
    "Set-Cookie": [
      `session=${auth.auth.token}`,
      "HttpOnly",
      "Secure",
      "SameSite=Lax",
      "Path=/",
      `Max-Age=${60 * 60 * 8}`,
    ].join("; "),
  },
});

Verifying the session

Decoding exp and trusting it is not authentication — a forged cookie passes. Verify the signature against the issuer's JWKS on every protected request.

import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL(process.env.GSB_JWKS_URL));

export async function verifySession(request: Request) {
  const token = readCookie(request, "session");
  if (!token) throw new UnauthorizedError();

  const { payload } = await jwtVerify(token, jwks, {
    issuer: process.env.GSB_TOKEN_ISSUER,
    audience: process.env.GSB_TOKEN_AUDIENCE,
  });

  // Tenant comes from the verified claim, never from the request.
  return {
    token,
    userId: String(payload.uid),
    tenantCode: String(payload.tc),
  };
}

jwtVerify checks the signature, exp, nbf, issuer, and audience together. A path-prefix check in middleware is routing, not security.

Authorizing the request

export async function GET(request: Request) {
  const session = await verifySession(request);

  // A verified identity is not yet an authorized one.
  if (!(await canRead(session.userId, session.tenantCode, "Order"))) {
    return new Response("Forbidden", { status: 403 });
  }

  const orders = new QueryParams<Order>("Order").skip(0).take(25);
  const page = await entityService.query(
    orders,
    session.token,
    session.tenantCode,
  );

  return Response.json(page);
}

Pass session.tenantCode explicitly. A tenant code read from a query string, header, or request body is caller-controlled input and must never reach a service call.

Errors

Status Body Cause
401 { "status": 401, "message": "Invalid credentials" } Wrong email or password
400 { "status": 400, "message": "Tenant code is required" } variation.tenantCode omitted
401 { "status": 401, "message": "Token has expired" } Expired token; re-authenticate

Rules

  • Server-only secrets. Anything behind NEXT_PUBLIC_ or VITE_ is compiled into the browser bundle and is public.
  • HttpOnly; Secure; SameSite=Lax cookies set by a server route. Never localStorage.
  • Verify the signature against JWKS on every protected request. Never decode and trust.
  • Resolve tenant context from the verified claim, then authorize the operation on that tenant.
  • Grant the narrowest role that works. Keep read-only inspection separate from mutation.
  • Validate every external input with a schema before it reaches a service call.
  • Redact tokens, credentials, and personal data from logs. Carry a request id instead.
  • Always HTTPS. Handle revocation and expiry explicitly.