Token strategy
Skill almasumdev/awesome-mobile-backend-agent-skills/.github/skills/auth/token-strategy
Agent skills for the backend-for-mobile layer: APIs, auth, push, sync, and BaaS integrations.
npx -y skills add almasumdev/awesome-mobile-backend-agent-skills --skill token-strategyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Choose between access+refresh tokens and opaque server sessions for mobile; design revocation and device binding. Use when deciding the token model for a new mobile backend or hardening an existing one.
SKILL.md
5.2 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it
Token Strategy for Mobile
Instructions
There are two defensible token models for mobile: access + refresh tokens (OAuth-style) and opaque server sessions (cookie-like). Pick one deliberately; mixing them creates revocation holes.
1. Access + Refresh (Recommended for Most Apps)
- Access token: short-lived (5–15 min), signed JWT, carried in
Authorization: Bearer <jwt>. - Refresh token: long-lived (30–90 days), opaque, rotated on every use, server stores only a hash.
- Best when there are multiple resource services that verify tokens independently.
- Revocation is soft -- you rely on short access-token TTL plus a denylist for urgent cases.
2. Opaque Server Sessions
- A single random session id (ULID or 32-byte random) stored in server-side session store (Redis, DB).
- Client sends it in
Authorization: Bearer <sid>or a secure cookie (for WebView contexts). - Best when you control a single API surface and want instant revocation.
- Every request costs a session-store lookup; cache carefully (in-process short-TTL) to stay fast.
3. Decision Matrix
| Need | Access + Refresh | Opaque Session |
|---|---|---|
| Multi-service fan-out without central lookup | best | adds latency |
| Instant global logout | denylist needed | native |
| Offline-ish clients that batch requests | fine | fine |
| Third-party API exposure (OAuth) | native | awkward |
| Simple backends, one API | overkill | best |
4. Revocation Design
For access + refresh:
- Refresh rotation detects reuse; revokes the session family on replay.
- Denylist (Redis set) of revoked
sidvalues with TTL = access-token max lifetime. Resource services check on each request (cheap, in-memory). - Publish revocation events (Kafka/Redis pubsub) so edge caches can purge.
For opaque sessions:
- Session row carries
revoked_at,last_seen_at. ADELETE /sessions/{id}immediately invalidates. - Cache session lookups in-process for ≤ 60 s; accept the eventual-consistency window.
5. Device Binding
Regardless of model, bind every credential to a device_id:
// Node/TS
async function login(userId: string, deviceId: string) {
const family = await sessions.createFamily({ userId, deviceId });
const access = signJwt({ sub: userId, sid: family.id, device_id: deviceId, exp: now() + 900 });
const refresh = randomBytes(32).toString("base64url");
await sessions.storeRefresh({ familyId: family.id, hash: hmac(refresh) });
return { access_token: access, refresh_token: refresh, expires_in: 900 };
}
A stolen token reused from a different device (detected via attestation or client-supplied device_id that the server re-verifies) triggers family revocation.
6. Client Handling
iOS (Swift):
actor TokenStore {
private var access: String?
private var refresh: String? // stored in Keychain across sessions
func authorize(_ request: inout URLRequest) async throws {
if try await isExpiringSoon() { try await refreshTokens() }
request.setValue("Bearer \(access!)", forHTTPHeaderField: "Authorization")
}
}
Android (Kotlin, OkHttp Authenticator for 401 retry):
class AuthInterceptor(private val tokens: TokenStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().newBuilder()
.header("Authorization", "Bearer ${tokens.access()}")
.build()
return chain.proceed(req)
}
}
class RefreshAuthenticator(private val tokens: TokenStore) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.responseCount >= 2) return null
val newAccess = runBlocking { tokens.refresh() } ?: return null
return response.request.newBuilder().header("Authorization", "Bearer $newAccess").build()
}
}
Key rules on the client:
- Refresh tokens live in Keychain (iOS) / EncryptedSharedPreferences (Android). Never plain storage.
- Only one refresh in flight; serialize concurrent refreshes (mutex /
actor). - On
invalid_grant, drop both tokens and force re-login.
7. Clock Skew
Treat the server clock as truth. Refresh access tokens when exp - now() < 60s, not when you get a 401 -- that avoids user-visible pauses.
8. Token Leakage Mitigations
- Never log full tokens; log only the last 6 chars.
- Pin TLS certificates for the auth endpoint if your threat model includes MITM.
- On suspected compromise (jailbreak/root detected + sensitive action), force re-auth.
Checklist
- Model chosen (access+refresh vs opaque session) and documented.
- Refresh tokens stored hashed with device + family binding.
- Denylist (Redis) for urgent access-token revocation, TTL = access lifetime.
- Client refreshes proactively based on
exp, not reactively on 401 only. - Single-flight refresh enforced on client.
- Tokens stored in Keychain / EncryptedSharedPreferences.
- Logs redact token material to last 6 chars.
- Revocation propagation (pubsub) documented for resource services.