session-management

Session Management — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "session-management" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-session-management

Session Management — Expert Decisions

Expert decision frameworks for session management choices. Claude knows Keychain basics and OAuth concepts — this skill provides judgment calls for security levels, refresh strategies, and cleanup requirements.

Decision Trees

Token Storage Strategy

Where should you store authentication tokens? ├─ Access token (short-lived, <1hr) │ └─ Keychain with kSecAttrAccessibleAfterFirstUnlock │ Available after first unlock, survives restart │ ├─ Refresh token (long-lived) │ └─ Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly │ More secure, device-bound, requires unlock │ ├─ Session ID (server-side session) │ └─ Keychain with kSecAttrAccessibleAfterFirstUnlock │ Needs to work for background refreshes │ ├─ Temporary auth code (OAuth flow) │ └─ Memory only (no persistence) │ Used once, discarded immediately │ └─ Remember me preference └─ UserDefaults (not sensitive) Just a boolean, not a credential

The trap: Storing tokens in UserDefaults. It's unencrypted, backed up to iCloud, and readable by jailbroken devices.

Token Refresh Architecture

How should you handle token refresh? ├─ Simple app, few API calls │ └─ Refresh on 401 response │ Reactive: refresh when expired │ ├─ Frequent API calls │ └─ Proactive refresh before expiration │ Schedule refresh 5 min before exp │ ├─ Real-time features (WebSocket) │ └─ Background refresh + reconnect │ Maintain connection continuity │ ├─ Offline-first app │ └─ Longer token lifetime + retry queue │ Queue requests when offline │ └─ High-security app └─ Short tokens + frequent refresh Minimize exposure window

Multi-Session Architecture

How many sessions does your app support? ├─ Single device, single account │ └─ Simple SessionManager singleton │ Replace tokens on new login │ ├─ Single device, multiple accounts (switching) │ └─ Account-keyed Keychain storage │ Keychain items per account ID │ Active account pointer │ ├─ Multiple devices, single account │ └─ Server-side session management │ Device tokens registered with server │ Remote logout capability │ └─ Multiple devices, multiple accounts └─ Full session registry Server tracks all device-account pairs Cross-device session visibility

Logout Cleanup Scope

What needs clearing on logout? ├─ Always clear │ └─ Tokens (Keychain) │ └─ User object (memory) │ └─ Authenticated state │ ├─ Usually clear │ └─ URL cache (cached API responses) │ └─ HTTP cookies │ └─ User preferences tied to account │ ├─ Consider clearing │ └─ Downloaded files (if user-specific) │ └─ Core Data (if user-specific) │ └─ Image cache (if contains private content) │ └─ Usually keep └─ App preferences (theme, language) └─ Onboarding completion state └─ Device registration

NEVER Do

Token Storage

NEVER store tokens in UserDefaults:

// ❌ Unencrypted, backed up, exposed on jailbreak UserDefaults.standard.set(accessToken, forKey: "accessToken") UserDefaults.standard.set(refreshToken, forKey: "refreshToken")

// ✅ Use Keychain try KeychainHelper.shared.save(accessToken, service: "auth", account: "accessToken") try KeychainHelper.shared.save(refreshToken, service: "auth", account: "refreshToken")

NEVER log or print tokens:

// ❌ Tokens in console logs — security disaster print("Token: (accessToken)") Logger.debug("Refresh token: (refreshToken)")

// ✅ Log safely Logger.debug("Token refreshed successfully") // No token content Logger.debug("Token length: (accessToken.count)") // Metadata only

NEVER hardcode secrets:

// ❌ Secrets in binary — extractable let clientSecret = "abc123xyz789" let apiKey = "sk-live-xxxxx"

// ✅ Use environment or server // Fetch from server during OAuth flow // Or use Info.plist with .gitignore for dev keys let clientId = Bundle.main.infoDictionary?["CLIENT_ID"] as? String

Token Refresh

NEVER retry refresh infinitely:

// ❌ Infinite loop if refresh token is invalid func refreshToken() async throws { do { let response = try await API.refresh(token: refreshToken) storeTokens(response) } catch { try await refreshToken() // Recursive retry — infinite loop! } }

// ✅ Limited retries with backoff, then logout func refreshToken(attempt: Int = 0) async throws { guard attempt < 3 else { await MainActor.run { logout() } throw SessionError.refreshFailed }

do {
    let response = try await API.refresh(token: refreshToken)
    storeTokens(response)
} catch {
    try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
    try await refreshToken(attempt: attempt + 1)
}

}

NEVER refresh on every request:

// ❌ Unnecessary API calls func makeRequest(_ endpoint: Endpoint) async throws -> Data { try await refreshAccessToken() // Refresh EVERY request! return try await performRequest(endpoint) }

// ✅ Refresh only when needed (expired or 401) func makeRequest(_ endpoint: Endpoint) async throws -> Data { if isTokenExpired() { try await refreshAccessToken() }

let (data, response) = try await performRequest(endpoint)

if (response as? HTTPURLResponse)?.statusCode == 401 {
    try await refreshAccessToken()
    return try await performRequest(endpoint).0
}

return data

}

Logout

NEVER forget to clear sensitive data:

// ❌ Partial cleanup — tokens still accessible func logout() { currentUser = nil isAuthenticated = false // Forgot to clear Keychain tokens! }

// ✅ Complete cleanup func logout() { // Clear tokens KeychainHelper.shared.deleteAll(service: keychainService)

// Clear memory
currentUser = nil
isAuthenticated = false

// Clear caches
URLCache.shared.removeAllCachedResponses()

// Clear cookies
HTTPCookieStorage.shared.removeCookies(since: .distantPast)

// Clear UserDefaults user data
let userKeys = ["userId", "userEmail", "userPreferences"]
userKeys.forEach { UserDefaults.standard.removeObject(forKey: $0) }

}

NEVER leave background tasks running after logout:

// ❌ Background refresh continues for logged-out user func logout() { clearTokens() currentUser = nil // Background refresh timer still running! }

// ✅ Cancel all background work func logout() { // Cancel scheduled tasks sessionRefreshTask?.cancel() sessionRefreshTask = nil

// Cancel any pending requests
URLSession.shared.getAllTasks { tasks in
    tasks.forEach { $0.cancel() }
}

// Clear data
clearTokens()
currentUser = nil

}

Keychain Security

NEVER use wrong accessibility level:

// ❌ Too permissive — accessible even when locked kSecAttrAccessibleAlways // Deprecated and insecure! kSecAttrAccessibleAlwaysThisDeviceOnly // Still too permissive

// ✅ Appropriate accessibility // For tokens that need background access: kSecAttrAccessibleAfterFirstUnlock

// For highly sensitive data (biometric): kSecAttrAccessibleWhenUnlockedThisDeviceOnly

NEVER ignore Keychain errors:

// ❌ Silent failure — user appears logged out func getToken() -> String? { let query = [...] var result: AnyObject? SecItemCopyMatching(query as CFDictionary, &result) // Ignoring status! return result as? String }

// ✅ Handle errors properly func getToken() throws -> String? { let query = [...] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result)

switch status {
case errSecSuccess:
    guard let data = result as? Data,
          let token = String(data: data, encoding: .utf8) else {
        throw KeychainError.invalidData
    }
    return token
case errSecItemNotFound:
    return nil  // No token stored
default:
    throw KeychainError.unableToRetrieve(status: status)
}

}

Essential Patterns

Secure SessionManager

@MainActor final class SessionManager: ObservableObject { static let shared = SessionManager()

@Published private(set) var isAuthenticated = false
@Published private(set) var currentUser: User?

private let keychainService = "com.app.auth"
private var refreshTask: Task&#x3C;Void, Never>?

private init() {
    restoreSession()
}

// MARK: - Authentication

func login(email: String, password: String) async throws {
    let response = try await AuthAPI.login(email: email, password: password)
    try storeTokens(access: response.accessToken, refresh: response.refreshToken)
    currentUser = response.user
    isAuthenticated = true
    scheduleTokenRefresh()
}

func logout() {
    // Cancel background work
    refreshTask?.cancel()
    refreshTask = nil

    // Clear Keychain
    KeychainHelper.shared.deleteAll(service: keychainService)

    // Clear state
    currentUser = nil
    isAuthenticated = false

    // Clear caches
    URLCache.shared.removeAllCachedResponses()
    HTTPCookieStorage.shared.removeCookies(since: .distantPast)
}

// MARK: - Token Management

func getAccessToken() -> String? {
    KeychainHelper.shared.read(service: keychainService, account: "accessToken")
}

func refreshAccessToken() async throws {
    guard let refreshToken = KeychainHelper.shared.read(
        service: keychainService, account: "refreshToken"
    ) else {
        throw SessionError.noRefreshToken
    }

    let response = try await AuthAPI.refresh(token: refreshToken)
    try storeTokens(access: response.accessToken, refresh: response.refreshToken)
}

// MARK: - Private

private func storeTokens(access: String, refresh: String) throws {
    try KeychainHelper.shared.save(access, service: keychainService, account: "accessToken")
    try KeychainHelper.shared.save(refresh, service: keychainService, account: "refreshToken")
}

private func restoreSession() {
    guard let _ = getAccessToken() else { return }
    isAuthenticated = true
    Task { try? await loadUserProfile() }
}

private func scheduleTokenRefresh() {
    refreshTask?.cancel()

    refreshTask = Task {
        while !Task.isCancelled {
            // Refresh 5 minutes before expiration
            try? await Task.sleep(nanoseconds: 55 * 60 * 1_000_000_000)  // 55 min
            guard !Task.isCancelled else { return }

            do {
                try await refreshAccessToken()
            } catch {
                await MainActor.run { logout() }
                return
            }
        }
    }
}

}

Secure KeychainHelper

final class KeychainHelper { static let shared = KeychainHelper() private init() {}

func save(_ value: String, service: String, account: String) throws {
    guard let data = value.data(using: .utf8) else {
        throw KeychainError.invalidData
    }

    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
    ]

    // Delete existing
    SecItemDelete(query as CFDictionary)

    // Add new
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.saveFailed(status: status)
    }
}

func read(service: String, account: String) -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &#x26;result)

    guard status == errSecSuccess,
          let data = result as? Data,
          let string = String(data: data, encoding: .utf8) else {
        return nil
    }

    return string
}

func delete(service: String, account: String) {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service,
        kSecAttrAccount as String: account
    ]
    SecItemDelete(query as CFDictionary)
}

func deleteAll(service: String) {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrService as String: service
    ]
    SecItemDelete(query as CFDictionary)
}

}

enum KeychainError: LocalizedError { case invalidData case saveFailed(status: OSStatus) case readFailed(status: OSStatus)

var errorDescription: String? {
    switch self {
    case .invalidData: return "Invalid data format"
    case .saveFailed(let status): return "Keychain save failed: \(status)"
    case .readFailed(let status): return "Keychain read failed: \(status)"
    }
}

}

Auto-Retry Network Client

actor NetworkClient { private let sessionManager: SessionManager

init(sessionManager: SessionManager = .shared) {
    self.sessionManager = sessionManager
}

func request&#x3C;T: Decodable>(_ endpoint: Endpoint) async throws -> T {
    var request = try endpoint.asURLRequest()

    // Add token
    if let token = await sessionManager.getAccessToken() {
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    }

    let (data, response) = try await URLSession.shared.data(for: request)

    // Handle 401 with retry
    if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 {
        try await sessionManager.refreshAccessToken()

        // Retry with new token
        if let newToken = await sessionManager.getAccessToken() {
            request.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization")
            let (retryData, _) = try await URLSession.shared.data(for: request)
            return try JSONDecoder().decode(T.self, from: retryData)
        }
    }

    return try JSONDecoder().decode(T.self, from: data)
}

}

Quick Reference

Keychain Accessibility Levels

Level When Accessible Use For

WhenUnlocked Device unlocked Foreground-only tokens

AfterFirstUnlock After first unlock Background refresh tokens

WhenUnlockedThisDeviceOnly Unlocked, no backup Highly sensitive data

WhenPasscodeSetThisDeviceOnly Passcode set Biometric-protected

Logout Cleanup Checklist

Data Storage Clear On Logout?

Access token Keychain ✅ Always

Refresh token Keychain ✅ Always

User profile Memory ✅ Always

API cache URLCache ✅ Usually

Cookies HTTPCookieStorage ✅ Usually

User preferences UserDefaults ⚠️ Maybe

Downloaded files FileManager ⚠️ If user-specific

App settings UserDefaults ❌ Usually keep

Token Refresh Strategies

Strategy When to Use Implementation

On 401 Simple apps Retry after refresh

Proactive Frequent API calls Timer before expiration

Background Real-time features BGAppRefreshTask

Red Flags

Smell Problem Fix

Tokens in UserDefaults Unencrypted storage Use Keychain

Logging token values Security exposure Log metadata only

Infinite refresh retry DoS on invalid token Limited retries + logout

Refresh on every request Unnecessary API calls Check expiration first

Partial logout cleanup Data leakage Clear all sensitive data

Ignoring Keychain errors Silent failures Handle status codes

kSecAttrAccessibleAlways Too permissive Use AfterFirstUnlock

Background tasks after logout Stale operations Cancel on logout

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review