SeleID

Quickstart

iOS (Swift)

Overview · React Native and Expo · Android · iOS · Register an app · API reference · Errors and security · Changelog

Coming soon @seletech/seleid-client is not on npm yet. These pages describe version 0.1.0 so you can plan an integration. See the roadmap.

There is no Swift SDK yet (Planned). The published TypeScript SDK does not run natively. Until a native SDK exists, iOS apps use the protocol directly. The snippets below show the protocol steps; they are not SeleID APIs and are untested against production, where sign-in for outside apps is not open.

The token endpoint answers with the standard snake_case fields (access_token, token_type, expires_in, id_token, refresh_token, scope), so a standard OpenID library such as AppAuth can parse it. Older camelCase aliases are still present for our own apps and are deprecated; do not rely on them. Sign-in for outside apps is Planned: it stays off until SeleTech switches it on for registered apps.

Endpoints

PurposeURL
Discoveryhttps://auth.seleid.dev/.well-known/openid-configuration
Authorize (browser)https://auth.seleid.dev/authorize
Token (POST, form-encoded)https://auth.seleid.dev/oauth/token (confidential apps: client_secret_basic or client_secret_post; public apps: PKCE only)
Userinfo (GET, Bearer access token; Planned, off by default)https://auth.seleid.dev/oauth/userinfo
Revocation (POST, RFC 7009; Planned, off by default)https://auth.seleid.dev/oauth/revoke
Session check (GET, Bearer access token; Planned, off by default)https://auth.seleid.dev/v1/session/context
Signing keyshttps://auth.seleid.dev/.well-known/jwks.json

Required authorize parameters: client_id, exact redirect_uri, response_type=code, scope containing openid, state, nonce (16 to 256 characters), code_challenge (S256, 43 to 128 characters). Token request: grant_type=authorization_code, client_id, code, redirect_uri, code_verifier; refresh uses grant_type=refresh_token, client_id, refresh_token. Refresh tokens rotate: persist the new one before using it, never replay a refresh request.

PKCE and the authorize request

Pkce.swift

import CryptoKit
import Foundation

func randomURLSafe(bytes: Int = 32) -> String {
    var data = Data(count: bytes)
    _ = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, bytes, $0.baseAddress!) }
    return base64URL(data)
}

func base64URL(_ data: Data) -> String {
    data.base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")
}

func challenge(for verifier: String) -> String {
    base64URL(Data(SHA256.hash(data: Data(verifier.utf8))))
}

SignIn.swift

import AuthenticationServices

var components = URLComponents(string: "https://auth.seleid.dev/authorize")!
components.queryItems = [
    .init(name: "client_id", value: clientId),
    .init(name: "redirect_uri", value: "com.example.app:/seleid"),
    .init(name: "response_type", value: "code"),
    .init(name: "scope", value: "openid profile email"),
    .init(name: "state", value: state),
    .init(name: "nonce", value: nonce),
    .init(name: "code_challenge", value: challenge(for: verifier)),
    .init(name: "code_challenge_method", value: "S256"),
]
let session = ASWebAuthenticationSession(url: components.url!, callbackURLScheme: "com.example.app") { callbackURL, error in
    guard let callbackURL, error == nil,
          let items = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems,
          items.first(where: { $0.name == "state" })?.value == state,
          let code = items.first(where: { $0.name == "code" })?.value else { return }
    // POST code + verifier to https://auth.seleid.dev/oauth/token (application/x-www-form-urlencoded)
}
session.presentationContextProvider = contextProvider
session.start()

Token storage

Store the refresh token in the Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (or stricter). Never use UserDefaults or log tokens.

Session checks

The server-side session check (GET /v1/session/context) is Planned. Until it exists, treat a refusal on refresh (invalid_grant) as signed out and a timeout or 5xx as “unknown” (retry, do not sign out). A Swift SDK mirroring the TypeScript one is Planned.

Last updated 2026-11-12Suggest an editReport a problem with this page