Problem
When using the MusicBrainz share extension to import a song, the user goes through the entire UI flow (extracting data, confirming import) before the main app is launched. Only then does the import fail with a 401 authentication error if the user isn't logged in.
Current flow:
- User shares from Safari/YouTube
- Extension extracts data and shows confirmation UI
- User confirms import
- Data saved to App Group, main app launched
- Main app attempts API call → 401 error (if not logged in)
Desired flow:
- User shares from Safari/YouTube
- Extension checks if user is authenticated
- If not authenticated → show "Please log in to Approach Note first" immediately
- If authenticated → proceed with normal flow
Technical Background
- Auth tokens are stored in the Keychain by the main app
- The share extension has a different bundle ID, so it can't access the main app's keychain items directly
- Both apps already share an App Group (
group.me.rodger.david.JazzReference) for passing import data
Option 1: Shared Keychain Access Group
Add a shared keychain access group so the extension can read the auth token directly.
Implementation
-
Update entitlements - Add keychain-access-groups to all targets:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)me.rodger.david.JazzReference.shared</string>
</array>
-
Update KeychainHelper.swift - Add access group to all keychain operations:
private let accessGroup = "TEAM_ID.me.rodger.david.JazzReference.shared"
// In save/load/delete methods, add:
kSecAttrAccessGroup as String: accessGroup
-
Apple Developer Portal - Register the new keychain access group
-
Migration - Existing users will need to re-login since tokens move to new access group
Pros
- Extension has direct access to verify token validity
- Could potentially make authenticated API calls from extension in the future
- Single source of truth for auth state
Cons
- Requires Apple Developer Portal configuration
- Existing users must re-login after update
- More complex keychain code
- Tokens accessible to extension (slightly larger attack surface)
Option 2: Store Auth Status in App Group UserDefaults (Recommended)
Store a simple "logged in" flag in the shared App Group UserDefaults. The extension checks this flag; actual tokens stay secure in main app's keychain.
Implementation
-
Create SharedAuthState helper (in Shared/):
class SharedAuthState {
private static let suiteName = "group.me.rodger.david.JazzReference"
private static let isAuthenticatedKey = "isAuthenticated"
private static let userDisplayNameKey = "userDisplayName"
static var isAuthenticated: Bool {
get {
UserDefaults(suiteName: suiteName)?.bool(forKey: isAuthenticatedKey) ?? false
}
set {
UserDefaults(suiteName: suiteName)?.set(newValue, forKey: isAuthenticatedKey)
}
}
static var userDisplayName: String? {
get {
UserDefaults(suiteName: suiteName)?.string(forKey: userDisplayNameKey)
}
set {
UserDefaults(suiteName: suiteName)?.set(newValue, forKey: userDisplayNameKey)
}
}
}
-
Update AuthenticationManager - Set flag on login/logout:
// In login success:
SharedAuthState.isAuthenticated = true
SharedAuthState.userDisplayName = user.displayName
// In logout:
SharedAuthState.isAuthenticated = false
SharedAuthState.userDisplayName = nil
-
Update share extensions - Check at startup in viewDidLoad/loadView:
override func viewDidLoad() {
super.viewDidLoad()
guard SharedAuthState.isAuthenticated else {
showLoginRequiredView()
return
}
// Continue with normal flow...
detectPageType()
}
-
Create LoginRequiredView - Simple UI explaining the user needs to log in:
struct LoginRequiredView: View {
let onDismiss: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "person.crop.circle.badge.exclamationmark")
.font(.system(size: 50))
Text("Login Required")
.font(.headline)
Text("Please open Approach Note and log in to import songs.")
.multilineTextAlignment(.center)
Button("OK") { onDismiss() }
}
}
}
Pros
- Simple implementation, no portal changes needed
- No migration needed for existing users
- Tokens stay secure in main app's keychain
- Can include user display name for personalized UI ("Logged in as Dave")
Cons
- Auth state could theoretically get out of sync (edge case)
- Extension can't verify token is actually valid (just that user logged in at some point)
- Can't make authenticated API calls from extension
Recommendation
Option 2 is recommended because:
- Simpler to implement
- No Apple Developer Portal changes
- No user migration/re-login required
- Sufficient for the use case (we just need to know if user is logged in)
The slight risk of state getting out of sync is minimal since:
- Login/logout are explicit user actions
- Worst case: user sees "not logged in" when they are → they open app and it works
- Or: user sees logged in but token expired → same 401 error as today, but rare
Files to Modify
Option 2 Implementation:
Problem
When using the MusicBrainz share extension to import a song, the user goes through the entire UI flow (extracting data, confirming import) before the main app is launched. Only then does the import fail with a 401 authentication error if the user isn't logged in.
Current flow:
Desired flow:
Technical Background
group.me.rodger.david.JazzReference) for passing import dataOption 1: Shared Keychain Access Group
Add a shared keychain access group so the extension can read the auth token directly.
Implementation
Update entitlements - Add
keychain-access-groupsto all targets:Update KeychainHelper.swift - Add access group to all keychain operations:
Apple Developer Portal - Register the new keychain access group
Migration - Existing users will need to re-login since tokens move to new access group
Pros
Cons
Option 2: Store Auth Status in App Group UserDefaults (Recommended)
Store a simple "logged in" flag in the shared App Group UserDefaults. The extension checks this flag; actual tokens stay secure in main app's keychain.
Implementation
Create SharedAuthState helper (in Shared/):
Update AuthenticationManager - Set flag on login/logout:
Update share extensions - Check at startup in
viewDidLoad/loadView:Create LoginRequiredView - Simple UI explaining the user needs to log in:
Pros
Cons
Recommendation
Option 2 is recommended because:
The slight risk of state getting out of sync is minimal since:
Files to Modify
Option 2 Implementation:
apps/Shared/Auth/SharedAuthState.swiftapps/Shared/Auth/AuthenticationManager.swift- set shared state on login/logoutapps/MusicBrainzImporter/ShareViewController.swift- check auth at startupapps/MusicBrainzImporterMac/MacShareViewController.swift- check auth at startupLoginRequiredViewfor iOS extensionMacLoginRequiredViewfor Mac extension