# Identity and Access

How a request proves who it is, and how the server decides what that identity
may do. Examples use TypeScript on Node with `jose` and Python with `PyJWT`;
the rules are the same in any stack.

## Contents

1. Choosing a credential type
2. Validating JWT access tokens
3. Refresh tokens and sessions
4. API keys for machine clients
5. Service-to-service identity
6. Authorization models
7. Enforcing authorization in code
8. Credential endpoints
9. Scoping a lookup by principal

## 1. Choosing a credential type

| Caller | Credential | Notes |
|---|---|---|
| Browser app on your own domain | HttpOnly, Secure, SameSite cookie session | Tokens never touch JavaScript; add CSRF protection for unsafe methods |
| Native mobile app | OAuth 2.1 authorization code + PKCE, short access token, rotating refresh token | Store in Keychain / Keystore, never plain preferences |
| Third-party developer integration | OAuth 2.1 with scoped consent, or scoped API keys | Let customers revoke per integration |
| Your own backend services | Workload identity (mTLS, cloud IAM, SPIFFE) or client-credentials tokens | No shared static secrets across services |

Avoid long-lived bearer tokens in browsers' `localStorage`; any XSS reads them.

## 2. Validating JWT access tokens

Validate the same way on every route, in one middleware, with a library.

```ts
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL("https://id.example.com/.well-known/jwks.json"));

export async function authenticate(req) {
  const header = req.headers.authorization ?? "";
  const [scheme, token] = header.split(" ");
  if (scheme !== "Bearer" || !token) throw unauthorized();

  const { payload } = await jwtVerify(token, jwks, {
    issuer: "https://id.example.com/",
    audience: "orders-api",
    algorithms: ["ES256"],
    clockTolerance: 30,
  });
  return { subject: payload.sub, accountId: payload.acc, scopes: String(payload.scope ?? "").split(" ") };
}
```

Rules the snippet encodes:

- **Algorithm allowlist.** The token header never picks the algorithm. This
  blocks `none` and RS256-to-HS256 key confusion.
- **Issuer and audience.** A token minted for another API must fail here.
- **Expiry with small skew.** Tens of seconds, not minutes.
- **Keys from JWKS with caching**, so rotation needs no deploy.
- **Asymmetric keys** (ES256, EdDSA, RS256) when more than one service verifies;
  a shared HMAC secret means every verifier can also mint.

Keep claims minimal. A JWT is signed, not encrypted; anyone holding it can read
the payload. Do not put email addresses, names or permissions that change often
into it; look volatile permissions up server-side.

Python equivalent of the core call:

```python
import jwt
from jwt import PyJWKClient

jwks = PyJWKClient("https://id.example.com/.well-known/jwks.json")

def verify(token: str) -> dict:
    key = jwks.get_signing_key_from_jwt(token).key
    return jwt.decode(
        token, key,
        algorithms=["ES256"],
        audience="orders-api",
        issuer="https://id.example.com/",
        leeway=30,
    )
```

## 3. Refresh tokens and sessions

- Refresh tokens are opaque random values stored hashed server-side, bound to a
  client and a device record.
- Rotate on every use: issue a new refresh token, invalidate the old one. If an
  already-used refresh token appears again, revoke the whole token family; it
  has been stolen.
- Give refresh tokens an absolute lifetime (for example 30 days) and an idle
  timeout.
- Logout, password change and "sign out everywhere" revoke refresh tokens. Access
  tokens then die at their short expiry; if you need instant revocation, keep a
  small deny list keyed by `jti` until expiry.

Cookie sessions: regenerate the session id at sign-in, set `HttpOnly`, `Secure`,
`SameSite=Lax` (or `Strict`), and scope `Path` and `Domain` tightly.

## 4. API keys for machine clients

```ts
import { randomBytes, createHash, timingSafeEqual } from "node:crypto";

export function mintKey() {
  const secret = randomBytes(32).toString("base64url");
  const key = `sk_live_${secret}`;
  return { key, prefix: key.slice(0, 12), hash: createHash("sha256").update(key).digest() };
}

export function matches(presented: string, storedHash: Buffer) {
  const hash = createHash("sha256").update(presented).digest();
  return hash.length === storedHash.length && timingSafeEqual(hash, storedHash);
}
```

- Show the full key once at creation; store only the hash and the prefix.
- Look up by prefix, compare the hash in constant time.
- Keys carry scopes and an owner, have an optional expiry, and log last use.
- A recognisable prefix (`sk_live_`) lets secret scanners and GitHub push
  protection catch leaks.

## 5. Service-to-service identity

- Prefer platform identity: Kubernetes service accounts with projected tokens,
  cloud IAM roles, or mTLS with certificates issued by a mesh or SPIFFE.
- When tokens are used, each service requests a token whose audience is the
  callee; do not forward the end user's token to every downstream service.
- If user context must travel, pass it as a separate signed assertion or as
  claims in a token exchange (RFC 8693), and have the callee still apply its own
  authorization.

## 6. Authorization models

| Model | Fits | Watch for |
|---|---|---|
| Ownership check | Personal data: my notes, my orders | Forgetting it on one route (BOLA) |
| RBAC (roles to permissions) | Staff tools, tiered plans | Role explosion; check permissions, not role names |
| ABAC / policy rules | Rules on attributes: region, status, amount | Policies scattered in code instead of one place |
| ReBAC (relationship graph) | Sharing and teams: "editor of folder X" | Needs a dedicated service (OpenFGA, SpiceDB) at scale |

Multi-tenant systems add one invariant above all others: every query is scoped
by tenant. Enforce it structurally (row-level security in Postgres, a repository
layer that requires a tenant id) so a missing `WHERE` cannot leak.

## 7. Enforcing authorization in code

Centralise decisions and keep them testable.

```ts
type Action = "order:read" | "order:cancel" | "order:refund";

export function can(principal: Principal, action: Action, order: Order): boolean {
  if (order.accountId !== principal.accountId) return false;
  switch (action) {
    case "order:read":
      return true;
    case "order:cancel":
      return order.status === "pending";
    case "order:refund":
      return principal.permissions.includes("refunds:write");
  }
}
```

- Deny by default: unknown actions return false.
- Load the object scoped by tenant first, then call `can` for the finer rule.
- Unit-test the policy table exhaustively; integration-test that each route
  calls it.

Postgres row-level security as a backstop:

```sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
  USING (account_id = current_setting('app.account_id')::uuid);
```

Set `app.account_id` per transaction from the authenticated principal, and make
sure the application role is not the table owner (owners bypass RLS unless
`FORCE ROW LEVEL SECURITY` is set).

## 8. Credential endpoints

Sign-in, password reset, OTP verification and sign-up are the most attacked
routes.

- Same response and similar timing whether the account exists or not.
- Rate limit per account identifier and per source; add progressive delays.
- Hash passwords with Argon2id (or bcrypt with cost 12 or more); check new
  passwords against a breached-password list rather than composition rules.
- Reset tokens: single use, short expiry (15 to 60 minutes), stored hashed.
- Offer passkeys (WebAuthn) and TOTP; treat SMS OTP as the weakest factor.
- Notify the user on sign-in from a new device and on credential changes.

## 9. Scoping a lookup by principal

```ts
// Scoped at the data layer: a foreign id simply is not found.
const order = await db.order.findFirst({
  where: { id: req.params.orderId, accountId: req.auth.accountId },
});
if (!order) return reply.code(404).send(problem("order-not-found"));
```
