SDK (iOS)¶
OmxusSignIn signs people in to your iOS app with an ordinary Omxus account
(the standard door). It opens the sign-in page in the system sign-in sheet,
keeps the session in the Keychain, and hands you a fresh access token when you
ask. A Swift package, no dependencies, iOS 16+ (macOS 13+ also builds).
Build and tests checked 30 Oct 2026: swift build clean, swift test 9 of 9
pass. The Swift samples below compile against the package's public API; the
full sign-in has not been run on a device from these docs.
Not at a public URL yet
The package is not published to a public repository (checked 30 Oct 2026).
Add it as a local package from a copy of the OmxusSignIn folder until it is.
1. Register the app¶
Once, as a public client: an app binary cannot keep a secret.
curl -X POST https://auth.omxus.com/oidc/register \
-H 'content-type: application/json' \
-d '{
"client_name": "Quantum Dice",
"door": "standard",
"token_endpoint_auth_method": "none",
"redirect_uris": ["com.example.quantum:/oauth/callback"]
}'
Keep the client_id. The redirect scheme must be your bundle id or another
reverse-DNS name you own (RFC 8252). Private-use schemes are refused for
confidential clients. All rules: Registering a site.
If the app shares people with a website that already signs in with Omxus, add
the app's redirect URI to the website's client and use that client_id, so
both see the same person. Or keep two clients and join them with a
sector_identifier_uri: see One person, several apps.
2. Add the package¶
Xcode: File > Add Package Dependencies > Add Local, then pick the
OmxusSignIn folder. No URL-scheme entry in Info.plist is needed: the
sign-in sheet catches the redirect itself.
3. Sign in¶
import OmxusSignIn
let omxus = OmxusSignIn(
issuer: URL(string: "https://auth.omxus.com")!,
clientId: "YOUR_CLIENT_ID",
redirectURI: URL(string: "com.example.quantum:/oauth/callback")!
)
// SwiftUI button action
Task {
let window = UIApplication.shared.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first!
account = try? await omxus.signIn(from: window)
}
signIn(from:) runs on the main actor and returns an Account.
Then¶
| Want | Call |
|---|---|
| Who is signed in (survives relaunch) | omxus.currentAccount, nil when signed out |
| Call your own API | try await omxus.accessToken(), sent as Authorization: Bearer ... |
| Sign out | omxus.signOut() |
| Sign out and wait for the revoke | await omxus.signOutAndWait() |
accessToken() refreshes first when the token has under 60 seconds left. When
the session is gone, or the provider refuses the refresh (invalid_grant or
401), it forgets the session and throws OmxusSignInError.signedOut: show
Sign in again.
signOut() forgets the session on the device at once and asks the provider to
revoke the refresh token in the background.
OmxusSignIn¶
OmxusSignIn(issuer: URL, clientId: String, redirectURI: URL,
scopes: [String] = ["openid", "profile", "email"])
| Property or method | What it does |
|---|---|
issuer, clientId, redirectURI, scopes |
as given; a trailing / on the issuer is dropped |
currentAccount: Account? |
the signed-in account, read from the Keychain at start-up |
signIn(from: ASPresentationAnchor) async throws -> Account |
the sign-in sheet, then the code exchange |
accessToken() async throws -> String |
a valid access token, refreshed when needed |
signOut() |
forget now, revoke in the background |
signOutAndWait() async |
forget now, return once the revoke was sent |
A second initialiser takes session: URLSession, store: TokenStore,
authenticator: WebAuthenticator and now: () -> Date, so every outside
dependency can be swapped in tests or previews.
Account¶
From the verified id_token.
| Field | Type | From claim |
|---|---|---|
id |
String |
sub |
username |
String? |
preferred_username |
email |
String? |
email |
emailVerified |
Bool? |
email_verified |
phoneNumber |
String? |
phone_number |
phoneNumberVerified |
Bool? |
phone_number_verified |
id is pairwise: stable for your app, different at every other site. See
How identity is derived. A refresh whose id_token
names a different sub is refused.
OmxusSignInError¶
| Case | When |
|---|---|
signedOut |
no session, or the refresh was refused; sign in again |
cancelled |
the person closed the sheet |
refused(String) |
the provider sent the person back with ?error= (the code is the string) |
stateMismatch |
the returned state is not the one sent |
noCode |
the callback has no code |
discovery |
the discovery document did not load, or names another issuer |
invalidIdToken(String) |
the id_token failed a check; the string says which (signature, issuer, audience, expired, nonce, alg, no key, jwks, subject changed, ...) |
tokenEndpoint(status:error:) |
the token endpoint answered an error other than a refused refresh |
Where tokens live¶
TokenStore is a protocol with load(), save(_:) and clear().
KeychainTokenStore(service:): the default. One generic-password item (serviceomxus.signin.<client_id>), this device only, readable after first unlock so a background refresh works.MemoryTokenStore: memory only, for previews and tests.
What it does on the wire¶
- Loads
/.well-known/openid-configurationand checks itsissuer. - Opens the authorization endpoint in
ASWebAuthenticationSessionwithresponse_type=code, PKCE S256,stateandnonce. The browser session is shared (not ephemeral), so saved passwords and passkeys for auth.omxus.com are offered. - Checks
state, andisswhen the callback carries one. - Exchanges the code at the token endpoint (
client_id+code_verifier, no secret). - Verifies the id_token with CryptoKit against the provider's JWKS: ES256
only, issuer, audience, expiry (60 s leeway),
iatnot in the future, nonce. - Stores access token, refresh token, expiry (
expires_in, 900 s if absent) and account as one Keychain item.
Refresh tokens rotate. The package runs one refresh at a time: two at once would look like a stolen token and end the session.
Endpoints: HTTP endpoints. The same flow in any language: Any app, via OIDC.