Base IdP

Base IdP

Frameworks

SwiftUI

Add Base IdP login to a SwiftUI app with the BaseIdP Swift package — PKCE over ASWebAuthenticationSession, a Keychain session, and a drop-in sign-in button.

SwiftUI apps sign in with the BaseIdP Swift package. It wraps ASWebAuthenticationSession (Apple's identity-aware browser session), does PKCE with CryptoKit, keeps the session in the Keychain, and gives you an ObservableObject you bind straight into your views. Public client, client id only, no secret on device.

Use the package, not copy-paste

There is no need to hand-roll the PKCE and callback plumbing anymore. The package at github.com/squareexp/base-idp-swift handles state validation, redirect matching, token refresh, and Keychain storage for you. This guide is the framework wiring around it — see the Swift SDK reference for the full API.

Add the package

In Xcode: File → Add Package Dependencies…, paste https://github.com/squareexp/base-idp-swift, and add the BaseIdP library to your app target. Requires iOS 15 / macOS 12 or newer.

Configure the app

Two Info.plist entries — your client id and your callback scheme.

Info.plist
<key>BASE_IDP_CLIENT_ID</key>
<string>sq_live_yourapp</string>

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.myapp.callback</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

You can also add the URL scheme visually under your target's Info → URL Types. The scheme (myapp, without ://) must match the scheme of the redirect URI registered in your Base IdP app exactly — this is where the "browser opens but never returns" bug lives.

Wire the auth manager

Hold one BaseIdPAuth at the root of your app with @StateObject and pass it into the environment.

MyApp.swift
import SwiftUI
import BaseIdP

@main
struct MyApp: App {
    @StateObject private var auth = BaseIdPAuth(clientId: "sq_live_yourapp")

    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(auth)
        }
    }
}

A sign-in view

SignInWithBaseIdPButton handles the whole flow — it disables itself and shows a spinner while authenticating.

RootView.swift
import SwiftUI
import BaseIdP

struct RootView: View {
    @EnvironmentObject private var auth: BaseIdPAuth

    var body: some View {
        if let user = auth.principal {
            VStack(spacing: 12) {
                Text("Signed in as \(user.name ?? user.email ?? user.id)")
                Button("Sign out") { auth.signOut() }
            }
        } else {
            VStack(spacing: 12) {
                SignInWithBaseIdPButton(auth: auth)
                if case .failed(let error) = auth.state {
                    Text(error.message).foregroundStyle(.red)
                }
            }
            .padding()
        }
    }
}

Because BaseIdPAuth restores its session from the Keychain on init, a returning user renders as signed in before the first frame — no loading flash.

Or drive it manually

Prefer your own button? Call signIn() — it is async and throws BaseIdPError.

Button("Sign in") {
    Task {
        do {
            let session = try await auth.signIn()
            print("welcome \(session.principal.email ?? "")")
        } catch let error as BaseIdPError where error.code == "login_cancelled" {
            // user backed out — ignore
        } catch {
            print(error)
        }
    }
}

Calling your API with a fresh token

validAccessToken() refreshes only when needed, then returns a usable token.

let token = try await auth.validAccessToken()
var request = URLRequest(url: URL(string: "https://api.example.com/me")!)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)

Handing off to your own backend

If your app has its own API, POST the session so the server can verify the Base token and merge it with product data:

let session = try await auth.signIn()
var request = URLRequest(url: URL(string: "https://api.example.com/auth/idp-exchange")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: session.serverHandoff())
_ = try await URLSession.shared.data(for: request)

See how the handoff works for the receiving side.

Things to watch

The browser opens but never comes back

A scheme mismatch. The redirect URI registered in Base IdP, your URL Types scheme, and the scheme the SDK derives must all agree exactly. This is the #1 setup bug.

confidential_client_in_mobile

Your client is registered confidential. Native apps must use a public client, or forward the code to a backend that holds the secret. See the SDK troubleshooting.

No such module 'BaseIdP'

The product isn't attached to your target. Add BaseIdP under General → Frameworks, Libraries, and Embedded Content, then reset package caches.

Repeated sign-ins reuse the same account

ASWebAuthenticationSession shares Safari cookies by default (silent SSO). For an account picker, create the manager with prefersEphemeralSession: true.

More failure modes

The Swift SDK reference has the full troubleshooting matrix — deployment targets, release-build differences, Keychain entitlements, and the complete BaseIdPError code table.

Where to go next

On this page