100 lines
2.6 KiB
Swift
100 lines
2.6 KiB
Swift
|
|
import Foundation
|
||
|
|
import Security
|
||
|
|
import Combine
|
||
|
|
|
||
|
|
final class AuthService: ObservableObject {
|
||
|
|
|
||
|
|
static let shared = AuthService()
|
||
|
|
|
||
|
|
@Published private(set) var isAuthenticated: Bool = false
|
||
|
|
|
||
|
|
private let service = "com.aamos.admin"
|
||
|
|
private let account = "jwt-token"
|
||
|
|
|
||
|
|
private init() {
|
||
|
|
isAuthenticated = load() != nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Public API
|
||
|
|
|
||
|
|
func save(token: String) throws {
|
||
|
|
guard let data = token.data(using: .utf8) else {
|
||
|
|
throw KeychainError.encodingFailed
|
||
|
|
}
|
||
|
|
|
||
|
|
delete()
|
||
|
|
|
||
|
|
let query: [CFString: Any] = [
|
||
|
|
kSecClass: kSecClassGenericPassword,
|
||
|
|
kSecAttrService: service,
|
||
|
|
kSecAttrAccount: account,
|
||
|
|
kSecValueData: data,
|
||
|
|
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||
|
|
]
|
||
|
|
|
||
|
|
let status = SecItemAdd(query as CFDictionary, nil)
|
||
|
|
guard status == errSecSuccess else {
|
||
|
|
throw KeychainError.saveFailed(status)
|
||
|
|
}
|
||
|
|
|
||
|
|
DispatchQueue.main.async { self.isAuthenticated = true }
|
||
|
|
}
|
||
|
|
|
||
|
|
func load() -> String? {
|
||
|
|
let query: [CFString: Any] = [
|
||
|
|
kSecClass: kSecClassGenericPassword,
|
||
|
|
kSecAttrService: service,
|
||
|
|
kSecAttrAccount: account,
|
||
|
|
kSecReturnData: true,
|
||
|
|
kSecMatchLimit: kSecMatchLimitOne
|
||
|
|
]
|
||
|
|
|
||
|
|
var result: AnyObject?
|
||
|
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||
|
|
|
||
|
|
guard status == errSecSuccess,
|
||
|
|
let data = result as? Data,
|
||
|
|
let token = String(data: data, encoding: .utf8)
|
||
|
|
else { return nil }
|
||
|
|
|
||
|
|
return token
|
||
|
|
}
|
||
|
|
|
||
|
|
@discardableResult
|
||
|
|
func delete() -> Bool {
|
||
|
|
let query: [CFString: Any] = [
|
||
|
|
kSecClass: kSecClassGenericPassword,
|
||
|
|
kSecAttrService: service,
|
||
|
|
kSecAttrAccount: account
|
||
|
|
]
|
||
|
|
|
||
|
|
let status = SecItemDelete(query as CFDictionary)
|
||
|
|
let deleted = status == errSecSuccess || status == errSecItemNotFound
|
||
|
|
|
||
|
|
if deleted {
|
||
|
|
DispatchQueue.main.async { self.isAuthenticated = false }
|
||
|
|
}
|
||
|
|
return deleted
|
||
|
|
}
|
||
|
|
|
||
|
|
func logout() {
|
||
|
|
delete()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Errors
|
||
|
|
|
||
|
|
enum KeychainError: LocalizedError {
|
||
|
|
case encodingFailed
|
||
|
|
case saveFailed(OSStatus)
|
||
|
|
|
||
|
|
var errorDescription: String? {
|
||
|
|
switch self {
|
||
|
|
case .encodingFailed:
|
||
|
|
return "Failed to encode token as UTF-8 data."
|
||
|
|
case .saveFailed(let status):
|
||
|
|
return "Keychain save failed with OSStatus \(status)."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|