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.
<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.
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.
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
React Native / Expo
Add Base IdP login to a React Native or Expo app — public client, PKCE, with expo-web-browser handling the system browser session.
Next.js (fullstack)
Server-rendered login in a Next.js app — all auth is server-side, tokens live in httpOnly cookies, no Base IdP key ever reaches the browser.