一键导入
token-storage
Handling access and refresh tokens on mobile — storage, rotation, revocation, and expiration. Use when wiring up the authenticated HTTP layer.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Handling access and refresh tokens on mobile — storage, rotation, revocation, and expiration. Use when wiring up the authenticated HTTP layer.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Biometric authentication on mobile using BiometricPrompt and LAContext, with keys cryptographically gated by user biometry. Use when adding unlock, re-auth, or step-up auth flows.
Multi-factor authentication on mobile — TOTP, push-based MFA, and recovery UX. Use when adding or reviewing a second factor.
OAuth 2.1 + PKCE for native mobile apps. Covers redirect URIs, AppAuth libraries, and the flows that are safe vs deprecated. Use when implementing or reviewing user login.
Anti-tampering on mobile — signature checks, runtime application self-protection (RASP), and realistic return on investment. Use to decide which integrity controls are worth shipping.
Code obfuscation on mobile — R8 / ProGuard on Android, SwiftShield and its limitations on iOS, and realistic expectations. Use when hardening release builds.
Detecting rooted Android and jailbroken iOS devices as a risk signal — not a silver bullet. Use when deciding which operations to allow on a compromised device.
| name | token-storage |
| description | Handling access and refresh tokens on mobile — storage, rotation, revocation, and expiration. Use when wiring up the authenticated HTTP layer. |
Treat tokens as short-lived credentials with a clear lifecycle. Storage alone is not enough — rotation and revocation matter just as much.
| Token | Lifetime | Where it lives |
|---|---|---|
| Access token (JWT or opaque) | 5–60 min | In-memory, optionally in Keychain for cold-start resume. |
| Refresh token | Hours → days (with rotation) | Keystore / Keychain only. |
id_token (OIDC) | Same as access | In memory; used for claims only, not for auth headers. |
Access tokens in memory survive backgrounding but are lost on process death — that is acceptable if refresh is cheap.
Rotation is mandatory for public clients (RFC 6749bis / OAuth 2.1):
suspend fun refresh(): TokenPair {
val current = secureStore.read("refresh") ?: throw NotAuthenticated
val next = api.refresh(current) // may throw 400 invalid_grant -> reuse detected
secureStore.write("refresh", next.refreshToken) // persist BEFORE using access token
return next
}
Multiple parallel 401s must trigger a single refresh, not N. Use a mutex:
private val refreshMutex = Mutex()
suspend fun authenticatedCall(block: suspend (String) -> Response): Response {
var token = memoryStore.access ?: refreshMutex.withLock { refreshIfNeeded() }
val resp = block(token)
if (resp.code == 401) {
token = refreshMutex.withLock { refreshIfNeeded(force = true) }
return block(token)
}
return resp
}
Same pattern works in Swift with an actor, in Dart with a Completer, and in JS with a shared Promise.
On logout:
On "logout everywhere" (user intent or suspected compromise) call the revoke_all_sessions endpoint or equivalent.
Audit all of these:
Authorization headers — most SDKs have a filter hook).final interceptor = QueuedInterceptorsWrapper(
onRequest: (req, h) async {
req.headers['Authorization'] = 'Bearer ${await tokenStore.accessToken()}';
h.next(req);
},
onError: (e, h) async {
if (e.response?.statusCode == 401) {
final newToken = await tokenStore.refresh();
final retried = await dio.fetch(e.requestOptions
..headers['Authorization'] = 'Bearer $newToken');
return h.resolve(retried);
}
h.next(e);
},
);
QueuedInterceptorsWrapper (not InterceptorsWrapper) serializes concurrent refresh attempts.