Unlocking the Secrets: A Beginner’s Journey into User Authentication with Keychain in SwiftUI

Alex-Stefan Albu
Yonder TechBlog
Published in
4 min readJan 25, 2024

--

Photo by Dmitry Chernyshov on Unsplash

Embarking on the journey of app development brings us face to face with various challenges, and one of the paramount ones is securing user data. As a budding developer delving into the realms of iOS app creation, understanding and implementing robust user authentication is both a fundamental and rewarding pursuit.

In this exploration, we’ll navigate the intriguing landscape of Keychain Services in SwiftUI — a foundational step towards creating secure and user-friendly authentication mechanisms. Join me on this learning odyssey, where we’ll uncover the mysteries of Keychain, enhance our understanding of secure credential storage, and build a SwiftUI app that embodies the essence of user data protection.

We’ll leverage the example of my small proof of concept to demonstrate practical implementation and demystify the intricacies of Keychain Services. Let’s dive into the intricacies of secure credential storage and learn how SwiftUI can be a powerful ally in crafting robust authentication systems.

What is the Keychain?

The Keychain is an encrypted container provided by Apple that ensures confidential information remains secure, even in the face of potential threats. Unlike UserDefaults, which is convenient but lacks the robust security measures necessary for sensitive data, the Keychain employs strong encryption algorithms to protect user credentials.

Why Keychain over UserDefaults?

  1. Encryption: Keychain data is encrypted, adding an extra layer of protection. UserDefaults, on the other hand, stores data in plain text, making it susceptible to unauthorised access.
  2. Secure Storage: UserDefaults is suitable for non-sensitive data like user preferences but lacks the security features crucial for storing sensitive information. Keychain, with its secure enclave, is purpose-built for safeguarding passwords and cryptographic keys.
  3. Accessibility: While UserDefaults data is easily accessible and modifiable, Keychain data is more challenging to tamper with. This makes Keychain the preferred choice for storing critical information.
  4. Persistent Storage: UserDefaults is prone to being wiped when an app is uninstalled or updated. Keychain, however, persists through such app lifecycle events, ensuring data consistency.

In the ever-evolving landscape of app development, maintaining data consistency across various and also keeping the app usable if anything unexpected happens is a challenge that often demands ingenious solutions. As I delved into a side project, the need arose to discern if an user was using the application for the first time after an update/reinstall.

Keychain, however, persists through such app lifecycle events, ensuring data consistency.

Naturally, I was not able to determine if an user was using the application for the first time by using the Keychain alone. In pursuit of a reliable solution, UserDefaults became the extra layer of insight needed. While Keychain excelled in securing data persistently, it needed a companion to answer the nuanced question of the first app launch. UserDefaults stepped in as the key to solving this puzzle.

How did I solve this issue?

Glad you asked! Here’s a step-by-step breakdown of the approach:

1. Keychain for Secure Credential Storage:

  • It ensures the persistence and security of data, making it a reliable choice for storing passwords.
  • Below are a few code example of different features(storing a Keychain password, fetching a Keychain password and deleting a password for an user)
func savePassword(_ password: String, for username: String) {
guard let data = password.data(using: .utf8) as? AnyObject else {
return
}

let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "passwordManager",
kSecAttrAccount as String: username,
kSecUseDataProtectionKeychain as String: kCFBooleanTrue!,
kSecValueData as String: data
]

SecItemAdd(query as CFDictionary, nil)
}
func loadPassword(for username: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "passwordManager",
kSecAttrAccount as String: username,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)

if status == errSecSuccess {
if let data = result as? Data {
return String(data: data, encoding: .utf8)
}
}

return nil
}
func deletePassword(for username: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "passwordManager",
kSecAttrAccount as String: username,
kSecUseDataProtectionKeychain as String: kCFBooleanTrue!
]
SecItemDelete(query as CFDictionary)
}

2. UserDefaults for the First Time User Check:

  • UserDefaults was introduced as a secondary mechanism to determine the first-time launch.
  • On the initial app launch, a specific key-value pair was set in UserDefaults, acting as a marker for the first login.
final class AppInitiationService {
private let isAppInitiatedKey = "isAppInitiated"

private var isAppInitiated: Bool? {
get {
UserDefaults.standard.bool(forKey: isAppInitiatedKey)
}

set {
UserDefaults.standard.set(newValue, forKey: isAppInitiatedKey)
}
}

var isAppOnFirstStart: Bool {
isAppInitiated != nil && isAppInitiated == false
}

func set(isAppInitiated: Bool) {
self.isAppInitiated = isAppInitiated
}
}

3. The Synergy of Two:

  • The combined use of Keychain and UserDefaults formed a symbiotic relationship, ensuring both the security of sensitive information and the ability to discern the first app login.
struct ContentView: View {
let appInitiationService = AppInitiationService()
let authenticationServiceInstance = AuthenticationService.shared

init() {
if appInitiationService.isAppOnFirstStart {
authenticationServiceInstance.clearKeychain()
appInitiationService.set(isAppInitiated: true)
}
}

var body: some View {
LoginView()
}
}

This article presents a simplified approach to a critical aspect of the implementation — actual solutions, implemented in real applications will and should be more complex than what this article presents. Here is a link to the entire codebase of this proof of concept here. I would really appreciate it if you can make the effort to start my repository! 🥳🤩

Thank you for making it this far! While acknowledging that better solutions exist, this exploration aimed to simplify the integration of Keychain and UserDefaults. As developers, our collective journey involves sharing insights, regardless of the complexity. This article serves as a stepping stone, contributing to the vast pool of shared knowledge. Remember, every bit of shared wisdom lights the path for someone, somewhere, in the future. Happy coding! 🤙🏻

Later edit: I also created a Youtube video detailing what I did in this article:
https://www.youtube.com/watch?v=rU6R5hDnTKQ

--

--