mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
191 lines
5.4 KiB
Swift
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 {}
|
|
}
|