Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungThe Model Got Better. Your Judgment Got Worse.(22.09.2026 um 03:02 Uhr)
Sichere ProgrammierungAIFeed - signed content permissions for AI web crawlers(22.09.2026 um 03:10 Uhr)
Sichere ProgrammierungThanks, glad you liked it!(22.09.2026 um 03:15 Uhr)
Sichere ProgrammierungA request for /.env shouldn't render your React app(22.09.2026 um 03:17 Uhr)
Sichere ProgrammierungAI-Agent Marketplaces Need Verifiable Delivery, Not More Listings(22.09.2026 um 03:20 Uhr)
AI & KI NachrichtenBeyond Bigger Models: Toward a Modular Cognitive Architecture(22.09.2026 um 03:21 Uhr)
Sichere ProgrammierungMasa Depan Manajemen Data: Mengenal Konsep Data Mesh yang Revolusioner(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungHow to Search Your Claude Code Conversation History(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungThe Model Got Better. Your Judgment Got Worse.(22.09.2026 um 03:02 Uhr)
Sichere ProgrammierungAIFeed - signed content permissions for AI web crawlers(22.09.2026 um 03:10 Uhr)
Sichere ProgrammierungThanks, glad you liked it!(22.09.2026 um 03:15 Uhr)
Sichere ProgrammierungA request for /.env shouldn't render your React app(22.09.2026 um 03:17 Uhr)
Sichere ProgrammierungAI-Agent Marketplaces Need Verifiable Delivery, Not More Listings(22.09.2026 um 03:20 Uhr)
AI & KI NachrichtenBeyond Bigger Models: Toward a Modular Cognitive Architecture(22.09.2026 um 03:21 Uhr)
Sichere ProgrammierungMasa Depan Manajemen Data: Mengenal Konsep Data Mesh yang Revolusioner(22.09.2026 um 03:22 Uhr)
Sichere ProgrammierungHow to Search Your Claude Code Conversation History(22.09.2026 um 03:22 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Two credentials, two threat models: auth for a content API

A headless content API has two kinds of callers, and it's tempting to secure them the same way. That's the mistake. There's a human logging into an admin UI to edit content, and there's a machine — a website, a build step — pulling pub…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

A headless content API has two kinds of callers, and it's tempting to secure them the same way. That's the mistake.



There's a human logging into an admin UI to edit content, and there's a machine — a website, a build step — pulling published content through a delivery endpoint. They authenticate with different credentials, and those credentials have opposite properties. Treat them identically and you either make the machine path painfully slow or the human path dangerously weak.



I built a small headless content API (Depot) partly to get this boundary right. Here's the reasoning.






The two credentials



A session proves "this human is logged in." Short-lived, rides in an httpOnly cookie, checked on management routes.



A delivery token proves "this machine may read this account's published content." Long-lived, sent as a Bearer header, checked on every public read.



Two auth surfaces, kept explicit:




/**
* - requireUser() — admin session (httpOnly JWT cookie) for the management API.
* - requireToken() — a `depot_…` bearer token for the public delivery API.
*/










Why they get different hashing



Here's the part people get wrong. Both credentials get stored as hashes — never plaintext — but not the same kind of hash, and the reason is entropy.



Passwords are low-entropy. Humans pick summer2024. An attacker who steals your DB will brute-force guesses against the stored hashes, so you want hashing to be deliberately slow — that's exactly what bcrypt's cost factor buys you:




import bcrypt from "bcryptjs";

const ROUNDS = 10; // deliberately slow — the point is to resist brute force

export function hashPassword(plain: string): Promise<string> {
return bcrypt.hash(plain, ROUNDS);
}

export function verifyPassword(plain: string, hash: string): Promise<boolean> {
return bcrypt.compare(plain, hash);
}






Tokens are high-entropy. I generate them — 32 random bytes — so there's nothing to guess. A stolen hash can't be reversed by brute force because the keyspace is astronomically large. That means I don't need bcrypt's slowness, and I very much don't want it: this hash runs on every single delivery request. So tokens get a fast SHA-256:




/**
* A token is `depot_<43 base64url chars>` — 32 bytes of entropy. Because it's
* high-entropy we store a fast SHA-256 hash (not bcrypt): bcrypt is for
* low-entropy human passwords; for random tokens SHA-256 is safe and fast
* enough to hash on every delivery request. The plaintext is shown once.
*/

export function generateToken(): string {
const bytes = crypto.getRandomValues(new Uint8Array(32));
return PREFIX + toBase64Url(bytes);
}

export async function hashToken(token: string): Promise<string> {
const data = new TextEncoder().encode(token);
const digest = await crypto.subtle.digest("SHA-256", data);
return toHex(new Uint8Array(digest));
}






The rule of thumb: bcrypt for what humans choose, SHA-256 for what you generate. Using bcrypt on a per-request delivery token would add real latency to every read for zero security gain. Using SHA-256 on a password would be a serious vulnerability. Same "hash the secret" instinct, opposite implementations.



One more detail: the plaintext token is shown once, at creation. After that only the hash exists in the DB. If the database leaks, the tokens in it are useless — same guarantee as passwords.






The guard, and the ownership check hiding in it



The token guard does the read-path auth. Note what it returns:




export async function requireToken(req: Request) {
const token = bearer(req);
if (!token || !looksLikeToken(token)) throw unauthorized("Provide a delivery token");

const hash = await hashToken(token);
const row = await db.query.apiTokens.findFirst({
where: eq(apiTokens.tokenHash, hash),
});
if (!row) throw unauthorized("Invalid delivery token");

// Fire-and-forget last-used bookkeeping; never block the read on it.
void db.update(apiTokens)
.set({ lastUsedAt: new Date() })
.where(eq(apiTokens.id, row.id))
.catch(() => {});

return { userId: row.userId, allowedOrigins: row.allowedOrigins };
}






Two things worth calling out.



It returns userId, not just "valid: true". Every downstream query scopes to that user. A valid token for account A can never read account B's content — the ownership boundary is carried by the auth result itself, not re-checked ad hoc in each route. Getting this wrong is how multi-tenant APIs leak data across tenants.



Last-used tracking is fire-and-forget. I want to know when a token was last used, but I will not make a content read wait on a bookkeeping write — void + .catch(() => {}) means it updates opportunistically and can never fail the request it's attached to.






Rate limiting, isolated so it's testable



The delivery API is public, so it needs a rate limit. The interesting decision here isn't the algorithm (fixed window) — it's that the algorithm is a pure function with no framework imports, so it can be unit-tested in isolation:




/**
* Pure fixed-window rate-limiting algorithm — no framework imports, so it can
* be unit-tested under `node --test`. `rate-limit.ts` wraps this with the HTTP
* concerns (throwing 429s, deriving the client key).
*/

export function checkLimit(key: string, opts: RateLimit, now = Date.now()) {
const existing = buckets.get(key);
if (!existing || now >= existing.resetAt) {
const resetAt = now + opts.windowMs;
buckets.set(key, { count: 1, resetAt });
return { ok: true, remaining: opts.limit - 1, resetAt };
}
existing.count += 1;
const remaining = Math.max(0, opts.limit - existing.count);
return { ok: existing.count <= opts.limit, remaining, resetAt: existing.resetAt };
}






The now = Date.now() default is the whole trick: tests inject a fixed clock and assert window behaviour deterministically, with no setTimeout and no flakiness. Security-adjacent logic is exactly the code you want pinned by fast, boring tests.






CORS, scoped per token



The delivery API is meant to be called from browsers, so it needs CORS. Blanket Access-Control-Allow-Origin: * on an authenticated endpoint is the lazy answer. Instead each token carries its own allowedOrigins — the guard returns them, and the response echoes the origin only if it's on that token's allowlist. The reach of a token is scoped at the token level, not globally.






The takeaway



Auth isn't a layer you add at the end — it's a set of decisions about the shape of your credentials:





  • Match the hash to the secret's entropy. bcrypt (slow) for human passwords, SHA-256 (fast) for high-entropy generated tokens.


  • Show generated secrets once, store only the hash. A DB leak should hand the attacker nothing usable.


  • Carry the ownership boundary in the auth result, so tenant isolation isn't re-litigated per route.


  • Isolate security-adjacent logic (rate limiting) from the framework so it's deterministically testable.
    None of these are exotic. They're just decisions that are cheap to make correctly up front and expensive to retrofit.






Depot is open source — code and a live demo. It's part of a set of Contentful/Next.js projects I keep public at vitaliipopov.dev.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Two credentials, two threat models: auth for a content API

Thematisch verwandte Begriffe: credentials, threat, models, auth · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-49449 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick