agentsclimarketplace

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.

Install
npx -y skills add almasumdev/awesome-mobile-backend-agent-skills --skill token-strategy

Assembled 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

NeedAccess + RefreshOpaque Session
Multi-service fan-out without central lookupbestadds latency
Instant global logoutdenylist needednative
Offline-ish clients that batch requestsfinefine
Third-party API exposure (OAuth)nativeawkward
Simple backends, one APIoverkillbest

4. Revocation Design

For access + refresh:

  • Refresh rotation detects reuse; revokes the session family on replay.
  • Denylist (Redis set) of revoked sid values 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. A DELETE /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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.