3
votes

I'm building an iOS app for my website and I'm attempting to use OAuth2 to manage login credentials. On user login, I'm successfully hitting my authentication endpoint with the provided username and password and I'm attempting to store both the Access Token and the Refresh Token in the keychain, so the user doesn't have to provide credentials moving forward.

I'm having trouble storing both refresh token and access token in my keychain, following instructions from these sources:

I'm able to successfully store either the Access Token or the Refresh Token, but no matter which one I store first, when attempting to store the other, I receive the following error message: "The specified item already exists in the keychain."

I added a CheckForExisting function to delete any existing items with the same specifications, but when I attempt to delete the existing keychain item using the same query, I receive a errSecItemNotFound status. So, frustratingly enough, I'm being told that I can't create my item because it already exists, but I can't delete the existing item because no existing item exists.

My hypothesis is that the creation of the Access Token item blocks the creation of the Refresh Token item, so I'm hoping someone can shed some light on the following:

  1. Why is the second item creation being blocked? Does the Keychain have some built in primary key checks that I'm hitting (like can't store more than one kSecClassInternetPassword)?
  2. What's the proper way to differentiate between the two tokens. Right now I'm using kSecAttrLabel, but that's a shot in the dark.

Please note that I'm hoping for an explanation of why my current approach is failing. I absolutely welcome alternative implementations, but I really want to understand what is going on behind the scenes here, so if possible please include an explanation of where an alternative implementation avoids the pitfalls that I seem to have fallen prey to.

Swift4 Code to Store the Tokens:

func StoreTokens(username: String, access_token: String, refresh_token: String) throws {
    func CheckForExisting(query: [String: Any]) throws {
        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            let error_message = SecCopyErrorMessageString(status, nil)!
            throw KeychainError.unhandledError(status: error_message)
        }
    }

    let configuration = ConfigurationDetails()

    let server = configuration.server
    let access_token = access_token.data(using: String.Encoding.utf8)!
    let refresh_token = refresh_token.data(using: String.Encoding.utf8)!
    let access_token_query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrAccount as String: username,
        kSecAttrServer as String: server,
        kSecAttrLabel as String: "AccessToken",
        kSecValueData as String: access_token
    ]

    let refresh_token_query: [String: Any] = [
        kSecClass as String: kSecClassInternetPassword,
        kSecAttrAccount as String: username,
        kSecAttrServer as String: server,
        kSecAttrLabel as String: "RefreshToken",
        kSecValueData as String: refresh_token
    ]

    try CheckForExisting(query: access_token_query)
    let access_status = SecItemAdd(access_token_query as CFDictionary, nil)
    guard access_status == errSecSuccess else {
        let error_message = SecCopyErrorMessageString(access_status, nil)!
        throw KeychainError.unhandledError(status: error_message)
    }

    try CheckForExisting(query: refresh_token_query)
    let refresh_status = SecItemAdd(refresh_token_query as CFDictionary, nil)
    guard refresh_status == errSecSuccess else {
        let error_message = SecCopyErrorMessageString(refresh_status, nil)!
        throw KeychainError.unhandledError(status: error_message)
    }
}
1

1 Answers

1
votes

According this https://developer.apple.com/documentation/security/errsecduplicateitem looks like the unique key for class kSecClassInternetPassword contains only these properties: kSecAttrAccount, kSecAttrSecurityDomain, kSecAttrServer, kSecAttrProtocol, kSecAttrAuthenticationType, kSecAttrPort, and kSecAttrPath.

So, kSecAttrLabel is not in the list, and your refresh_token_query duplicates access_token_query.