Skip to content

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 (service omxus.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

  1. Loads /.well-known/openid-configuration and checks its issuer.
  2. Opens the authorization endpoint in ASWebAuthenticationSession with response_type=code, PKCE S256, state and nonce. The browser session is shared (not ephemeral), so saved passwords and passkeys for auth.omxus.com are offered.
  3. Checks state, and iss when the callback carries one.
  4. Exchanges the code at the token endpoint (client_id + code_verifier, no secret).
  5. Verifies the id_token with CryptoKit against the provider's JWKS: ES256 only, issuer, audience, expiry (60 s leeway), iat not in the future, nonce.
  6. 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.