Swift SDK (iOS / macOS)
Integrate MimiKeySDK in iOS and macOS apps — callback setup, authorization, picking, reading, creating, updating and revoking, plus the waiting UI and error handling.
Overview
MimiKeySDK is the Apple-platform implementation of third-party authorization v3. It switches to MimiKey, waits while the user acts on an approval card, exchanges and refreshes tokens, signs every call, and opens sealed results on the device. Your code calls a handful of async methods.
- Platforms: iOS 17+ and macOS 14+, Swift 5.9 toolchain.
- Dependencies: CryptoKit, Security and SwiftUI only; no third-party packages.
- Client type: public (no
client_secret; the token exchange uses PKCE).
Note To download the SDK source and a ready-to-run demo app, see SDK download & demo app.
Before you begin
- In MimiKey for Mac, open Developer access (开发者接入) in the sidebar and register a public client whose redirect URL uses your custom scheme, for example
com.example.ai://mimikey/callback. Note theclient_id(pmc_…). See Getting a client_id. - Install MimiKey on your test device and sign in.
Installation
In Xcode, choose File → Add Package Dependencies…, click Add Local…, select the unzipped MimiKeySDK folder, and add the MimiKeySDK product to your app target.
With Package.swift:
// swift-tools-version:5.9
dependencies: [
.package(path: "../MimiKeySDK"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "MimiKeySDK", package: "MimiKeySDK"),
]),
]
Configure Info.plist
You need two entries: register your callback scheme, and declare that you query the mimikey scheme (the SDK uses it to detect whether MimiKey is installed).
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.ai</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.example.ai</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mimikey</string>
</array>
- The callback URL must exactly match one of the registered
redirect_uris. - On iOS, without
LSApplicationQueriesSchemesthe SDK always concludes MimiKey is missing and throwsnotInstalled. - macOS also needs
CFBundleURLTypes.LSApplicationQueriesSchemesis harmless there; the SDK usesNSWorkspaceto detect the app. - Sandboxed Mac apps need the
com.apple.security.network.cliententitlement.
Create the client
Keep one MimiKeyClient instance for your whole app:
import MimiKeySDK
let mimikey = MimiKeyClient(configuration: MimiKeyConfiguration(
clientID: "pmc_xxxxxxxxxxxxxxxx",
redirectURL: URL(string: "com.example.ai://mimikey/callback")!
))
Parameters of MimiKeyConfiguration(clientID:redirectURL:origin:downloadURL:):
| Parameter | Default | Description |
|---|---|---|
clientID | — | Your registered client_id |
redirectURL | — | A registered redirect URL |
origin | https://password.tminos.com | The MimiKey server |
downloadURL | https://password.tminos.com | Where the notInstalled error points the user |
MimiKeyClient.init also accepts optional store (custom secret storage; the system keychain by default), urlSession, appSwitcher, pollInterval (2 seconds by default) and authorizeTimeout (600 seconds by default). You normally only set these in tests.
Hand callbacks to the SDK
Pass every URL your app receives to handleCallback(_:). It matches the waiting flow by state; for anything that is not a MimiKey callback, or whose state does not match, it returns false and ignores the URL.
SwiftUI (iOS and macOS)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
mimikey.handleCallback(url)
}
}
}
}
UIKit
// SceneDelegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
mimikey.handleCallback(context.url)
}
}
AppKit
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager.shared().setEventHandler(
self,
andSelector: #selector(handleGetURL(_:withReplyEvent:)),
forEventClass: AEEventClass(kInternetEventClass),
andEventID: AEEventID(kAEGetURL)
)
}
@objc func handleGetURL(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) {
guard let string = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
let url = URL(string: string) else { return }
mimikey.handleCallback(url)
}
Note Callback URLs carry only
code,request_id,stateandstatus— never ciphertext or plaintext. Results are always fetched by the SDK with a signed request and opened with the seal private key on the device.
Authorize
let grant = try await mimikey.authorize()
print(grant.grantID, grant.approvalKeyFingerprint, grant.vaults)
authorize():
- Generates a seal key pair (X25519), an approval key pair (Ed25519), a PKCE verifier and a
state, all unique to this authorization; - Opens
mimikey://authorize, where MimiKey shows the consent card; - Waits for the callback (up to
authorizeTimeout, 600 seconds by default) and exchanges thecodeand PKCE verifier for tokens; - Saves keys and tokens to the keychain and returns a
MimiKeyGrant.
MimiKeyGrant property | Description |
|---|---|
grantID | Grant ID |
accountID | The authorizing user's account ID |
vaults | Vaults the user selected ([MimiKeyVault] with id, name, kind, mode) |
approvalKeyFingerprint | Approval-key fingerprint (16 hex characters), identical to the one on the consent card |
Authorization completes only through the callback. If the user switches back to your app without acting in MimiKey, authorize() keeps waiting until it times out, you call cancelPending(), or its task is cancelled.
Read local state without a network call:
mimikey.isAuthorized // whether this device holds a usable grant
mimikey.grant // the MimiKeyGrant saved at authorization, or nil
mimikey.reset() // forget local credentials without telling the server (e.g. when your user signs out)
Calling authorize() again creates a new grant and replaces the one stored on the device.
Pick an item
let item = try await mimikey.pickItem(
hint: MimiKeyHint(url: "https://bank.example.com", kind: .login),
reason: "Sign in to Example Bank to reconcile statements"
)
item.id // item ID
item.revision // revision number
item.record.username
item.record.password // lives only in your process memory
MimiKey shows the pick card. Candidates come from the granted vaults and are ordered by hint (same registrable domain first, then title match, then same kind); the user can search. When the user taps Use this item, the result comes back sealed.
| Parameter | Description |
|---|---|
hint | MimiKeyHint(url:title:kind:), all optional; only affects ordering |
reason | Purpose shown on the card (up to 500 characters, visible to the server) |
vaultID | Optional; nil picks across every vault in the grant |
Read a known item
let same = try await mimikey.getItem(id: item.id, vaultID: grant.vaults[0].id)
askpolicy: MimiKey shows the read approval card and the user taps Allow once.trustpolicy: the request is approved on creation. Opening MimiKey executes it directly; the card only flashes "Provided per your trust setting".
Create an item
let created = try await mimikey.createItem(
kind: .login,
prefill: MimiKeyPrefill(
title: "Example Bank",
url: "https://bank.example.com/login",
usernameHint: "me@example.com"
),
reason: "Sign in to Example Bank to reconcile statements"
)
if let item = created.item {
// Saved into a granted vault: you receive the full item
} else {
// created.savedOutOfScope == true: saved to a vault outside the grant, no plaintext for this task
}
MimiKey shows the safety card: the full edit form for that item kind. The user types the password in MimiKey; your app never handles it.
| Parameter | Description |
|---|---|
kind | MimiKeyItemKind: .login, .creditCard, .bankCard, .apiKey, .server, .secureNote |
prefill | MimiKeyPrefill(title:url:usernameHint:extraHint:), prefilled into the form and editable by the user; never put credentials here |
reason | Purpose shown on the card |
vaultID | Optional; defaults to the grant's first vault |
MimiKeyCreated property | Description |
|---|---|
requestID | Request ID |
item | The full item; nil if saved outside the grant |
summary | MimiKeyItemSummary (id, title, url, kind), never password or notes |
savedOutOfScope | Whether the item was saved outside the grant |
If the same grant and vault already have a pending create request for the same URL host, the server returns that request instead of creating a new one, and the SDK waits on it.
Update an item
let updated = try await mimikey.updateItem(
id: item.id,
vaultID: grant.vaults[0].id,
patch: [.password: "new password"],
reason: "Password was changed on the website; sync it to MimiKey"
)
updated.itemID // item ID
updated.revision // new revision after the write
The SDK first creates a draft request to obtain the request_id, then seals the patch {"patch":{…}} to the account identity public key and uploads it; the server sees only ciphertext. On the update card the user sees the old and new value of each field and taps Save changes, after which the device writes a new revision.
Patch keys are MimiKeyField values: the standard .title, .username, .password, .url and .notes, or .extra("card_number") for a kind-specific field. Your app does not receive the updated plaintext, only the new revision number.
Revoke
try await mimikey.revoke()
Calls POST /api/oauth/revoke (signed with the approval key) and then wipes every credential from the keychain. If the grant is already gone on the server, revoke() still wipes local state and returns normally.
Waiting UI
After MimiKey opens, the user may switch back to your app without doing anything. The SDK waits for the callback, polls the request every 2 seconds, and checks immediately when your app returns to the foreground; the server's expires_at sets the deadline. Your waiting screen must offer two ways out — Retry and Cancel:
@MainActor
final class Model: ObservableObject {
@Published var waiting: MimiKeyWaiting?
init() {
mimikey.onWaiting = { [weak self] w in self?.waiting = w } // called on the main thread
}
func retry() { Task { try? await mimikey.retryOpen() } } // reopen MimiKey
func cancel() { mimikey.cancelPending() } // the waiting call throws .cancelled
}
MimiKeyWaiting property | Description |
|---|---|
phase | .authorize or .request(op:) |
requestID | Request ID (nil while authorizing) |
waitingOn | Whose turn it is: user or device |
waitingReason | consent, approval, pick, input, review, no_unlocked_device |
expiresAt | Expiry time |
message | Reference wording in Chinese |
Suggested wording:
waitingOn / waitingReason | Message |
|---|---|
user / consent | Waiting for you to confirm in MimiKey |
user / approval | Waiting for you to approve this use in MimiKey |
user / pick | Waiting for you to pick an item in MimiKey |
user / input | Waiting for you to fill in and save the safety card in MimiKey |
user / review | Waiting for you to confirm the change in MimiKey |
device / no_unlocked_device | Approved — waiting for any of your devices to unlock MimiKey |
cancelPending() makes a best effort to set the server request to cancelled. Cancelling the Task that is awaiting the call has the same effect.
The optional MimiKeyButton renders a button that follows your app's styling, labelled 用 MimiKey 填入 ("Fill with MimiKey") by default; pass your own title to localize it:
MimiKeyButton("Fill with MimiKey") { Task { try await pick() } }
Error handling
Every method throws MimiKeyError:
| Error | Meaning | Suggested handling |
|---|---|---|
.notInstalled(downloadURL:) | MimiKey is not on this device | Send the user to downloadURL to install it |
.notAuthorized | No grant on this device | Call authorize() |
.cancelled | You cancelled the wait, or the request was cancelled on the server | No message needed |
.denied | The user declined on the card | Respect the decision; do not retry immediately |
.expired | The code or request expired, or no result arrived in time | Let the user try again |
.notSignedIn | MimiKey has no signed-in account | Ask the user to sign in to MimiKey first |
.revoked | The grant was revoked, the token family was invalidated, or the integrator was disabled; the SDK has wiped local credentials | Ask the user to authorize() again |
.conflict | The item changed elsewhere during the update | Fetch the latest item and update again |
.network(Error) | A network request failed | Retry later |
.protocol(String) | Any other protocol or server error; the string is the error code | Log the code; see Errors |
do {
let item = try await mimikey.pickItem(reason: "Sign in to Example Bank")
use(item)
} catch MimiKeyError.notInstalled(let url) {
showInstallPrompt(url)
} catch MimiKeyError.revoked, MimiKeyError.notAuthorized {
_ = try await mimikey.authorize()
} catch MimiKeyError.denied, MimiKeyError.cancelled {
// The user's decision — stay quiet
} catch {
showError(error.localizedDescription)
}
Tokens are managed for you: when fewer than 30 seconds remain on the access token, the SDK refreshes it first; if a call returns token_expired, it refreshes and retries once. refresh_token_reused, grant_revoked, client_disabled and similar codes surface as .revoked and are never retried automatically.
Note If the item was changed elsewhere while the update card was open, MimiKey keeps the user's version and denies the request with reason
conflict; the SDK throws.conflict. Fetch the item again withgetItem, then resubmit the change.
Keychain storage
The SDK stores each client's credentials as generic-password items in the system keychain:
| Item | Value |
|---|---|
| service | com.tminos.mimikey.sdk.<clientID> |
| Accessibility | kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (never synced, never migrated to another device) |
| Contents | Approval-key seed, seal private key, access and refresh tokens with expiry, grant_id, account_id, account identity public key, granted vault list |
To share credentials between your app and its extensions, pass MimiKeyKeychain(clientID:accessGroup:) as the store. The SDK writes no logs, caches no plaintext, and zeroes decryption buffers after use.
Testing
- Unit tests: inject
MimiKeyMemoryStore()as thestoreand your ownMimiKeyAppSwitchingimplementation in place of real app switching to test your flows without MimiKey. - Simulator: the iOS Simulator usually has no MimiKey installed, so calls throw
notInstalled. For end-to-end tests, use a physical device or a Mac with MimiKey installed. - Demo app:
Examples/MimiKeyDemowalks through authorize, pick, create, update and revoke. See SDK download & demo app.