stack/sdks/implementations/swift/Sources/StackAuth/TokenStore.swift
Konstantin Wohlwend 66b066db6e Swift SDK
2026-01-19 13:14:13 -08:00

191 lines
5.4 KiB
Swift

import Foundation
import Security
/// Protocol for custom token storage implementations
public protocol TokenStoreProtocol: Sendable {
func getAccessToken() async -> String?
func getRefreshToken() async -> String?
func setTokens(accessToken: String?, refreshToken: String?) async
func clearTokens() async
}
/// Token storage configuration
public enum TokenStore: Sendable {
/// Store tokens in Keychain (default, secure, persists across launches)
case keychain
/// Store tokens in memory (lost on app restart)
case memory
/// Explicit tokens (for server-side usage)
case explicit(accessToken: String, refreshToken: String)
/// No token storage
case none
/// Custom storage implementation
case custom(any TokenStoreProtocol)
}
// MARK: - Keychain Token Store
actor KeychainTokenStore: TokenStoreProtocol {
private let projectId: String
private let accessTokenKey: String
private let refreshTokenKey: String
init(projectId: String) {
self.projectId = projectId
self.accessTokenKey = "stack-auth-access-\(projectId)"
self.refreshTokenKey = "stack-auth-refresh-\(projectId)"
}
func getAccessToken() async -> String? {
return getKeychainItem(key: accessTokenKey)
}
func getRefreshToken() async -> String? {
return getKeychainItem(key: refreshTokenKey)
}
func setTokens(accessToken: String?, refreshToken: String?) async {
if let accessToken = accessToken {
setKeychainItem(key: accessTokenKey, value: accessToken)
} else {
deleteKeychainItem(key: accessTokenKey)
}
if let refreshToken = refreshToken {
setKeychainItem(key: refreshTokenKey, value: refreshToken)
} else {
deleteKeychainItem(key: refreshTokenKey)
}
}
func clearTokens() async {
deleteKeychainItem(key: accessTokenKey)
deleteKeychainItem(key: refreshTokenKey)
}
// MARK: - Keychain Helpers
private func getKeychainItem(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let string = String(data: data, encoding: .utf8) else {
return nil
}
return string
}
private func setKeychainItem(key: String, value: String) {
guard let data = value.data(using: .utf8) else { return }
// First try to update
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let attributes: [String: Any] = [
kSecValueData as String: data
]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecItemNotFound {
// Item doesn't exist, add it
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
SecItemAdd(addQuery as CFDictionary, nil)
}
}
private func deleteKeychainItem(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary)
}
}
// MARK: - Memory Token Store
actor MemoryTokenStore: TokenStoreProtocol {
private var accessToken: String?
private var refreshToken: String?
func getAccessToken() async -> String? {
return accessToken
}
func getRefreshToken() async -> String? {
return refreshToken
}
func setTokens(accessToken: String?, refreshToken: String?) async {
self.accessToken = accessToken
self.refreshToken = refreshToken
}
func clearTokens() async {
self.accessToken = nil
self.refreshToken = nil
}
}
// MARK: - Explicit Token Store
actor ExplicitTokenStore: TokenStoreProtocol {
private let accessToken: String
private let refreshToken: String
init(accessToken: String, refreshToken: String) {
self.accessToken = accessToken
self.refreshToken = refreshToken
}
func getAccessToken() async -> String? {
return accessToken
}
func getRefreshToken() async -> String? {
return refreshToken
}
func setTokens(accessToken: String?, refreshToken: String?) async {
// Explicit tokens are immutable
}
func clearTokens() async {
// Explicit tokens are immutable
}
}
// MARK: - Null Token Store
actor NullTokenStore: TokenStoreProtocol {
func getAccessToken() async -> String? { nil }
func getRefreshToken() async -> String? { nil }
func setTokens(accessToken: String?, refreshToken: String?) async {}
func clearTokens() async {}
}