Group Group Group Group Group Group Group Group Group

Keychain + access and refresh tokens

expectation: deleting the app and reinstalling would re-auth the user automatically
what is happening: having to login again when I delete and re-install the app

using the Keychain wrapper like below and update the tokens using computed properties

extension CurrentSession {
  private func getToken(service: String) -> String? {
    let kcw = KeychainWrapper()
    if let token = try? kcw.getTokenFor(service: service) {
      return token
    }
    
    return nil
  }
  
  func updateToken(service: String, token: String) {
    let kcw = KeychainWrapper()
    do {
      try kcw.storeTokenFor(service: service, token: token)
    } catch let error as KeychainWrapperError {
      print("Exception setting token: \(error.message ?? "no message")")
    } catch {
      print("An error occurred setting the token.")
    }
  }
}

what am I doing wrong?

import Foundation

struct KeychainWrapperError: Error {
  var message: String?
  var type: KeychainErrorType
  
  enum KeychainErrorType {
    case badData
    case servicesError
    case itemNotFound
    case unableToConvertToString
  }
  
  // `OSStatus`: can assume one of the values listed in Item Return Result Keys
  // https://developer.apple.com/documentation/security/1542001-security_framework_result_codes
  init(status: OSStatus, type: KeychainErrorType) {
    self.type = type
    if let errorMessage = SecCopyErrorMessageString(status, nil) {
      self.message = String(errorMessage)
    } else {
      self.message = "Status Code: \(status)"
    }
  }
  
  init(type: KeychainErrorType) {
    self.type = type
  }
  
  init(message: String, type: KeychainErrorType) {
    self.message = message
    self.type = type
  }
}

final class KeychainWrapper {
  func storeTokenFor(account: String, service: String, token: String) throws {
    if token.isEmpty {
      try deleteTokenFor(account: account, service: service)
      return
    }
    
    guard let tokenData = token.data(using: .utf8) else {
      print("Error converting value to data.")
      throw KeychainWrapperError(type: .badData)
    }
    
    // The query is a dictionary that maps a String to an Any object, depending on the attribute.
    //  This pattern is common when calling C-based APIs from Swift. For each attribute, you supply the defined global
    //  constant beginning with kSec. In each case, you cast the constant to a String (it’s a CFString really), and you follow it with the value for that attribute.
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrAccount as String: account,
                                kSecAttrService as String: service,
                                kSecValueData as String: tokenData]
    
    // Asks Keychain Services to add information to the keychain
    // You cast the query to the expected CFDictionary type.
    // C APIs often use the return value to show the result of a function. Here the value has type OSStatus.
    let status = SecItemAdd(query as CFDictionary, nil)
    
    switch status {
    case errSecSuccess:
      break
    case errSecDuplicateItem:
      try updateTokenFor(account: account, service: service, token: token)
    default:
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }
  
  func getTokenFor(account: String, service: String) throws -> String {
    let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrAccount as String: account,
                                kSecAttrService as String: service,
                                kSecMatchLimit as String: kSecMatchLimitOne,
                                kSecReturnAttributes as String: true,
                                kSecReturnData as String: true]
    
    var item: CFTypeRef?
    // The C function will update the memory at that location with a new value
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    
    guard status != errSecItemNotFound else {
      throw KeychainWrapperError(type: .itemNotFound)
    }
    
    guard status == errSecSuccess else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
    
    // Cast the returned CFTypeRef to a dictionary
    guard
      let existingItem = item as? [String: Any],
      let valueData = existingItem[kSecValueData as String] as? Data,
      let value = String(data: valueData, encoding: .utf8)
    else {
      throw KeychainWrapperError(type: .unableToConvertToString)
    }
    
    return value
  }
  
  func updateTokenFor(account: String, service: String, token: String) throws {
    guard let tokenData = token.data(using: .utf8) else {
      print("Error converting value to data.")
      return
    }
    
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecAttrService as String: service
    ]
    
    let attributes: [String: Any] = [
      kSecValueData as String: tokenData
    ]
    
    let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    guard status != errSecItemNotFound else {
      throw KeychainWrapperError(
        message: "Matching Item Not Found",
        type: .itemNotFound)
    }
    guard status == errSecSuccess else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }
  
  func deleteTokenFor(account: String, service: String) throws {
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecAttrService as String: service
    ]
    
    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }
  
}