Skip to content

Share extension: Check authentication before import flow #92

@dprodger

Description

@dprodger

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:

  1. User shares from Safari/YouTube
  2. Extension extracts data and shows confirmation UI
  3. User confirms import
  4. Data saved to App Group, main app launched
  5. Main app attempts API call → 401 error (if not logged in)

Desired flow:

  1. User shares from Safari/YouTube
  2. Extension checks if user is authenticated
  3. If not authenticated → show "Please log in to Approach Note first" immediately
  4. 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

  1. Update entitlements - Add keychain-access-groups to all targets:

    <key>keychain-access-groups</key>
    <array>
        <string>$(AppIdentifierPrefix)me.rodger.david.JazzReference.shared</string>
    </array>
  2. 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
  3. Apple Developer Portal - Register the new keychain access group

  4. 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

  1. 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)
            }
        }
    }
  2. 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
  3. 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()
    }
  4. 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:

  1. Simpler to implement
  2. No Apple Developer Portal changes
  3. No user migration/re-login required
  4. 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:

  • Create apps/Shared/Auth/SharedAuthState.swift
  • Update apps/Shared/Auth/AuthenticationManager.swift - set shared state on login/logout
  • Update apps/MusicBrainzImporter/ShareViewController.swift - check auth at startup
  • Update apps/MusicBrainzImporterMac/MacShareViewController.swift - check auth at startup
  • Create LoginRequiredView for iOS extension
  • Create MacLoginRequiredView for Mac extension

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions