# Share Extensions in Practice – Part 2: Secure Keychain Sharing Between App and Extension

Table of Contents

This is part 2 of a 4-part series on building robust iOS Share Extensions:


When your app and Share Extension need to work together, they must securely share sensitive data like authentication tokens. This is where iOS Keychain Sharing comes in. Both targets define a shared Access Group, secured by your Team ID and App Identifier Prefix.

Here’s the beauty: once configured in Xcode, your app and extension can access the same secure keychain vault through kSecAttrAccessGroup. Users don’t need to log in twice, and tokens flow seamlessly between processes.

The Keychain is perfect for secrets, but what about other data? For content or metadata from your Share Extension, you’ll need a different approach covered in the next part of this series.

Setting up Keychain SharingLink to heading

Configure Access GroupsLink to heading

The first step is configuring identical Access Groups in both targets. This is crucial - they must match exactly:

com.example.myapp.group
// In Xcode: Target → Signing & Capabilities → Keychain Sharing
// Format: $(AppIdentifierPrefix)com.yourcompany.yourapp.shared

For detailed steps, see Apple’s Adding Capabilities to Your App documentation.

Important: The Access Group format combines your Team ID (automatically prefixed) with your custom identifier. Both your main app and Share Extension must use the identical string.

Behind the scenes, these Access Groups are added to your app’s entitlements file as an array under the key keychain-access-groups. Each entry must start with your Team ID as a prefix (e.g., $$(AppIdentifierPrefix).com.example.group). This is how iOS recognizes which apps belong to the same developer team and grants access to shared Keychain data.

Finding Your Team IDLink to heading

Before we dive into the code, we need to find your Team ID since we’ll need it in the code to access the shared group.

To find your Team ID, you’ll need to check Apple’s Developer Center:

  1. Log in to Apple’s Developer Center
  2. Choose your team by clicking your name in the top-right corner
  3. Select “Membership” from the left panel
  4. Find your Team ID under the “Membership Information” section

Your Team ID is a 10-character alphanumeric string (like ABCD123456) that uniquely identifies your developer account or organization.

Developer Program RequirementsLink to heading

Note: Accessing the Team ID for Keychain sharing requires a paid Apple Developer Program membership, as Keychain Access Groups are only available to enrolled developers. Yes, it’s annoying – I couldn’t even start developing this feature until I had a subscription.

You can use Keychain for individual apps without a Team ID or paid membership through free provisioning profiles, but features like Keychain Sharing between multiple apps or permanent certificates require the Team ID and a paid membership.

Build a Keychain WrapperLink to heading

The raw Keychain API is notoriously complex. Let’s build a clean wrapper that handles the heavy lifting. Here we’ll use the Team ID we found earlier. This is the only place in your project where you’ll need to reference it directly:

import Security
class KeychainHelper {
static let shared = KeychainHelper()
private init() {}
// Replace with your actual access group
private static let accessGroup = "$TEAMID.com.yourapp.shared"
// MARK: - Public API
@discardableResult
func saveToken(_ token: String) -> Bool {
return saveString(token, forKey: "auth_token")
}
func loadToken() -> String? {
return loadString(forKey: "auth_token")
}
@discardableResult
func deleteToken() -> Bool {
return deleteItem(forKey: "auth_token")
}
}

This gives us a simple, focused API for token management. But let’s break down the implementation:

Saving to KeychainLink to heading

The save operation converts our string to data and stores it with the shared Access Group:

extension KeychainHelper {
@discardableResult
private func saveString(_ value: String, forKey key: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessGroup as String: KeychainHelper.accessGroup
]
// Delete existing item first to avoid duplicates
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
}

Key points:

  • kSecClass: We’re storing a generic password (most common for tokens)
  • kSecAttrAccount: Acts as our unique key identifier
  • kSecAttrAccessGroup: The magic ingredient for sharing between targets
  • SecItemDelete: Always delete first to avoid “duplicate item” errors

Loading from KeychainLink to heading

The load operation queries the shared Access Group and converts the returned data back to a string:

extension KeychainHelper {
private func loadString(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrAccessGroup as String: KeychainHelper.accessGroup,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
// Query the Keychain for matching items
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
return String(data: data, encoding: .utf8)
}
return nil
}
}

Key points:

  • kSecReturnData: Tell Keychain to return the actual data, not just metadata
  • kSecMatchLimit: Only return one result (there should only be one anyway)
  • Always check the status code before using results

Deleting from KeychainLink to heading

The delete operation removes the item from the shared Access Group:

extension KeychainHelper {
@discardableResult
private func deleteItem(forKey key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrAccessGroup as String: KeychainHelper.accessGroup
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
}

Note: errSecItemNotFound is treated as success since the item doesn’t exist anyway.

Using the Keychain HelperLink to heading

Now both your main app and Share Extension can use the same simple API:

// In your main app after login
let success = KeychainHelper.shared.saveToken("user_auth_token_here")
// In your Share Extension when making API calls
if let token = KeychainHelper.shared.loadToken() {
// Make authenticated API request
apiClient.authenticate(with: token)
} else {
// Handle missing token (show login prompt, etc.)
showLoginRequired()
}
// When user logs out (from either app or extension)
KeychainHelper.shared.deleteToken()

ConclusionLink to heading

Keychain Sharing provides a secure, seamless way to share authentication tokens between your app and Share Extension. Users stay logged in across both environments, and sensitive data remains protected by iOS’s security infrastructure.

The key is proper Access Group configuration and a clean wrapper API that hides Keychain’s complexity. With this foundation, your Share Extension can make authenticated API calls just like your main app.

Now that we’ve covered secure data sharing, the next article will explore how to share Core Data entities between apps. This is particularly interesting for non-sensitive data that needs to be accessible across both your main app and extension.

Next up: Part 3: App Groups & Core Data for sharing non-sensitive data between your main app and extension.


Further ReadingLink to heading

For more details on Keychain Services, check out Apple’s official documentation:

This “Share Extensions in Practice” series shows you step by step how to develop stable, secure, and user-friendly Share Extensions for iOS.

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts