Users & Auth  /  Authentication

Authentication

Sign users into your app, and let your server trust who they are. The client SDK opens a hosted sign-in page — no UI to build, no passwords to store — and your server verifies the resulting token with your secret key. Use whichever half you need, or both.

Applies to Client · frontend Server · backend

1Overview

Onelo Auth handles the whole sign-in flow for you. On the client, one view opens the Onelo-hosted page (email & password, or social sign-in with Google / GitHub / Apple on paid plans) and stores the returned tokens securely on-device. On the server, one call verifies a user's token against Onelo before you serve protected data. There is no login form to build and no password to store — Onelo never hands you a raw credential.

2How it works

01
Open hosted page

The SDK launches the Onelo sign-in page in a native window (or iframe/popup on web).

02
User signs in

Email & password, or OAuth on paid plans — all on Onelo’s page.

03
One-time code

A single-use code returns to your app — deep link (your app’s own unique callback scheme, from Settings) on native, postMessage on web.

04
Exchange for token

The SDK swaps the code for tokens, stored on device.

3Install & integrate

Read the panel for your stack — everything's shown, nothing's hidden behind a picker. Building the common frontend + backend flow? Both halves are below with the token handoff between them. Every snippet renders from @onelo/snippets — the same source the dashboard uses.

This is where you set up Onelo. You install & initialize the client once here; other modules (Features, Monitor) reuse the same client and show only their module-specific calls.
① Clientfrontend

Install

// Xcode → File → Add Package Dependencies: // https://github.com/onelo-tools/onelo-swift

Initialize

import OneloSwift

// 1. Register a URL scheme for the login callback.
//    In Xcode: target → Info → URL Types → add your scheme (e.g. "myapp").
//    This lets the hosted login page redirect back to your app after sign-in.

// 2. Set up auth in your App entry point
@main struct MyApp: App {
    @StateObject var auth = OneloAuth(
        config: .init(
            publishableKey: "onelo_pk_live_YOUR_KEY",
            apiUrl: URL(string: "https://api.onelo.tools")!,
            callbackScheme: "myapp"  // replace with your URL scheme from step 1
        )
    )
    var body: some Scene {
        WindowGroup {
            // OneloAuthView handles login, signup and password reset.
            //
            // ⚠️ THIS CLOSURE RUNS ONLY WHEN THE USER IS SIGNED IN **AND** ENTITLED
            //    — that is: your app has no paywall, or the user holds active
            //    access. A signed-in user WITHOUT a plan never reaches it; they
            //    are routed to your store (or, on Apple with the Access Gate on,
            //    to a "no active plan" screen).
            //
            //    So do NOT put "the user just signed in" side effects here —
            //    closing your own login window, starting a session, flipping app
            //    state. On a paywalled app they will never fire for a plan-less
            //    user and the login window stays open over a perfectly valid
            //    session.
            //
            //    DRIVING YOUR OWN WINDOWS? Observe auth.$isAllowedIn — the single
            //    signal for "this user should see the app". Do NOT rebuild it from
            //    currentSession: that means SIGNED IN, and a user with no plan is
            //    signed in while still belonging in the store or on "no active
            //    plan". Do NOT use hasActiveAccess either: it means HAS A PAID
            //    GRANT, so on an app with no paywall it is false for everyone and
            //    would lock out every legitimate user.
            //
            //      auth.$isAllowedIn
            //          .sink { allowed in if allowed { self.showApp() } }
            //          .store(in: &cancellables)
            //
            //    It already waits for the remote config, which is the trap in a
            //    hand-rolled version: paywallEnabled is false until config lands,
            //    so a naive check says "allowed in" to everyone right after launch.
            //    Needs onelo-swift 3.80.0 or newer.
            OneloAuthView(auth: auth) {
                ContentView()
                    .environmentObject(auth)
            }
            // 3. Finish sign-ins that come back from OUTSIDE the app.
            //    REQUIRED if you enable Magic link (dashboard → Auth →
            //    Passwordless). The email opens in the user's browser, which then
            //    hands the one-time code back over the scheme from step 1 —
            //    without this line the code arrives at your app and nothing
            //    catches it, so the link appears to do nothing.
            //    Harmless to include when magic link is off, and it also covers
            //    any future flow that returns from the system browser.
            //    Needs onelo-swift 3.79.1 or newer.
            .onOpenURL { url in
                Task { try? await auth.handleAuthCallback(url) }
            }
            //    AppKit app with an NSApplicationDelegate instead of SwiftUI
            //    scenes? Implement this on the delegate and call the same method:
            //      func application(_ application: NSApplication, open urls: [URL]) {
            //          for url in urls {
            //              Task { @MainActor in try? await onelo.auth.handleAuthCallback(url) }
            //          }
            //      }
            //    handleAuthCallback returns nil for URLs that aren't Onelo's, so
            //    you can pass every incoming URL through it without filtering.
            //
            //    On macOS it also brings your app to the front. The OS delivers the
            //    URL but does not focus you, so with the browser on another display
            //    or Space the sign-in would otherwise complete out of sight and look
            //    broken.
            //
            //    MENU-BAR / AGENT APPS (NSApp.setActivationPolicy(.accessory)):
            //    activating is not enough — an accessory app does not come forward
            //    like a normal one. Order your own window front as well:
            //      NSApp.setActivationPolicy(.regular)
            //      window.collectionBehavior.insert(.moveToActiveSpace)
            //      window.makeKeyAndOrderFront(nil)
            //    .moveToActiveSpace shows it on the desktop the user is looking at
            //    rather than switching them to another one.
        }
    }
}

Usage

// 4. OneloAuthView always opens the centrally-hosted sign-in page —
//    on both Free and Paid plans. Email / password stays inside the
//    embedded WKWebView. Social providers (Google, GitHub, Apple — paid
//    plans) are handed off to ASWebAuthenticationSession automatically — required
//    by Apple App Store Review and gives buyers their cached
//    SSO session in Safari, so no re-login + 2FA every time. Branding
//    (colors, logo, copy) is configured in the Onelo dashboard.

// 5. Access the current user anywhere in your app
struct ContentView: View {
    @EnvironmentObject var auth: OneloAuth

    var body: some View {
        VStack {
            if let user = auth.currentSession?.user {
                Text("Hello, \(user.email ?? "user")")
                Button("Sign out") {
                    Task { try? await auth.signOut() }
                }
            }
        }
    }
}

// ── Automatic remote-logout ─────────────────────────────────
// You don't need to write any code for this — OneloAuthView observes
// auth.currentSession (which is @Published) and AUTOMATICALLY shows
// the sign-in screen when the session is cleared remotely. Delivery
// is instant (sub-second) over a Server-Sent Events channel the SDK
// opens after sign-in, with a 13-minute heartbeat fallback in case
// the long-lived connection is blocked by a corporate firewall.
// Triggers that clear the local session:
//   • Customer self-requested account deletion via /customer/portal.
//   • Refund issued AND no eligible free-tier plan to fall back to
//     (the app you sell becomes unusable for that buyer; SDK kicks them
//     out so they can re-purchase or close the app).
//   • You suspended/banned the user in the Onelo admin dashboard.
//   • Hard-delete by the 30-day account-deletion cron job.
//
// REQUIREMENTS so the auto-logout actually shows the sign-in screen:
//   1. OneloAuthView MUST be your ROOT view — above any NavigationStack,
//      TabView, or custom router. If it's nested inside, navigation pop
//      stays on whatever screen the user was on when revoked.
//      ❌ NavigationStack { OneloAuthView(auth: auth) { ContentView() } }
//      ✅ OneloAuthView(auth: auth) { NavigationStack { ContentView() } }
//   2. In ContentView observe auth via @EnvironmentObject / @ObservedObject —
//      never capture it into a local 'let' (snapshot won't update on revoke).
//      ❌ let user = auth.currentSession?.user; if user != nil { … }
//      ✅ if let user = auth.currentSession?.user { … }
//   3. If you maintain your own NavigationPath, reset it on revoke so the
//      stack doesn't outlive the session:
//        .onChange(of: auth.currentSession) { newSession in
//            if newSession == nil { navigationPath = .init() }
//        }
//
// Full working example with NavigationStack + path reset:
//
//   @main struct MyApp: App {
//       @StateObject var auth = OneloAuth(config: .init(/* … */))
//       var body: some Scene {
//           WindowGroup {
//               // OneloAuthView wraps the whole NavigationStack so that on
//               // remote-revoke the entire stack is replaced by the hosted
//               // sign-in page — no leftover pushed screens.
//               OneloAuthView(auth: auth) {
//                   RootView().environmentObject(auth)
//               }
//           }
//       }
//   }
//
//   struct RootView: View {
//       @EnvironmentObject var auth: OneloAuth
//       @State private var path = NavigationPath()
//
//       var body: some View {
//           NavigationStack(path: $path) {
//               HomeView()
//                   .navigationDestination(for: Route.self) { /* … */ }
//           }
//           // Belt-and-suspenders: reset the path on revoke so deep
//           // links don't survive the logout. OneloAuthView already
//           // re-presents the sign-in page; this just clears the stack
//           // behind it so the user lands on HomeView after re-login.
//           .onChange(of: auth.currentSession) { newSession in
//               if newSession == nil { path = NavigationPath() }
//           }
//       }
//   }
//
// If you want to show a one-time "your account was deleted" toast
// before re-presenting the sign-in screen, observe the
// auth.isUserRevoked flag (also @Published):
//
//   .onChange(of: auth.isUserRevoked) { revoked in
//       if revoked { showAccountDeletedToast() }
//   }
//
// ── AppKit / multi-window apps ──────────────────────────────
// The "OneloAuthView as root" pattern above assumes a single SwiftUI
// WindowGroup. Menu-bar utilities, floating widgets, and multi-window
// editors (separate NSWindow instances for face / bubble / chat / auth)
// don't fit that shape — your main user-facing window is NOT the one
// hosting OneloAuthView, so the SwiftUI re-render on revoke can't
// hide it. Drive your windows from Combine instead.
//
// ⚠️ TWO DIFFERENT SIGNALS. Do not use one for both directions.
//
//   SHOW YOUR APP  →  auth.$isAllowedIn   (signed in AND allowed in)
//   HIDE YOUR APP  →  auth.$currentSession going non-nil → nil (revoke)
//
// Using $currentSession for the "show" direction is the mistake this warning
// exists for: a user with no plan on a paywalled app IS signed in, and letting
// them in on that signal walks straight past your paywall and past the Apple
// Access Gate. $isAllowedIn is false for them, and flips true by itself the
// moment they buy — which the session signal never does.
//
//   auth.$isAllowedIn
//       .removeDuplicates()
//       .sink { [weak self] allowed in
//           guard allowed else { return }
//           Task { @MainActor in self?.showApp() }   // close login window, bring UI up
//       }
//       .store(in: &cancellables)
//
// Needs onelo-swift 3.80.0 or newer. Below that, build it yourself as
//   isReady && currentSession != nil && (!paywallEnabled || hasActiveAccess)
// and note the isReady term: paywallEnabled is false until the remote config
// lands, so without it every user looks allowed in for the first moments.
//
// The revoke direction, which is a different edge entirely:
//
//   import Combine
//
//   private var cancellables = Set<AnyCancellable>()
//   private var hadSession = false
//   private var isSigningOut = false  // set true around your own auth.signOut() call
//
//   auth.$currentSession.sink { [weak self] session in
//       guard let self else { return }
//       let has = session != nil
//       defer { self.hadSession = has }
//       // Fire only on non-nil → nil (the actual revoke moment).
//       // Skip the user-initiated Sign out path you already handle.
//       if self.hadSession && !has && !self.isSigningOut {
//           Task { @MainActor in
//               self.mainWindow.orderOut(nil)
//               self.otherWindows.forEach { $0.orderOut(nil) }
//               self.showAuthWindow()   // your NSWindow hosting OneloAuthView
//           }
//       }
//   }.store(in: &cancellables)
//
// Note: OneloAuthView hosted in a hidden / minimized / off-screen
// NSWindow will NOT react to remote-revoke. Bring the window forward
// (orderFrontRegardless / makeKeyAndOrderFront) BEFORE you expect the
// hosted page to appear.

// ── Automatic legal-consent gate ────────────────────────────
// You don't need to write any code for this. When you publish a
// material update to your Terms of Service in the Onelo dashboard,
// OneloAuthView automatically shows the updated document — in the
// SAME hosted WKWebView used for sign-in — the next time the user
// opens the app after the effective date. The user must tap
// "Accept & Continue" to reach your content(), or "Sign out".
// There is no "Decline" button: nothing to mis-tap.
//   • Terms of Service → blocks the app until accepted (legal
//     requirement in DE/PL: silent "acceptance by continued use"
//     is not valid — an explicit tap is required).
//   • Privacy Policy / DPA / Cookies → users are notified by email
//     and the app is not blocked (GDPR information duty) — UNLESS you
//     published the version with "Require acceptance", which shows the
//     same blocking gate as Terms.
//
// This automatic gate only works WHILE OneloAuthView is mounted — i.e. when it
// wraps your content() as the root view (the pattern in step 2).
//
// ⚠️ IF YOU USE OneloAuthView ONLY FOR SIGN-IN and switch to your own UI after
// login (OneloAuthView is no longer in the view tree), the automatic gate
// cannot fire — nothing is left to host it. In that case add ONE line to your
// post-login root so the gate is enforced on YOUR UI:
//
//   struct HomeView: View {
//       @EnvironmentObject var auth: OneloAuth
//       var body: some View {
//           MyAppContent()
//               .oneloConsentGate(auth: auth)   // ← blocking gate on your own UI
//       }
//   }
//
// .oneloConsentGate presents a full-cover hosted gate OVER your UI when a
// blocking version is pending (Accept & Continue / Sign out, no dismiss). It
// re-checks on appear, on app-foreground, and on the real-time SSE push — so a
// running, logged-in app shows the gate instantly when you publish.
//
//   • Terms of Service → blocks the app until accepted (legal requirement in
//     DE/PL: silent "acceptance by continued use" is not valid).
//   • Privacy / DPA / Cookies → notified by email only; never blocks the app
//     (GDPR information duty) unless you published it with "Require acceptance".
//
// Advanced (optional): read pending consents yourself, e.g. for a custom banner —
//   let pending = await auth.requiredConsents()
//   let mustAccept = pending.filter { $0.blocking }   // blocking, past effective
// Recording acceptance from your own UI:
//   try await auth.acceptConsent(versionId: requirement.versionId)

// ── If using your own auth system ───────────────────────────
// When you have your own user database, call identify() after your login so the
// Features SDK can apply per-user/per-plan targeting. Without it, targeted features
// fall back to "hidden" and you'll see a console warning at runtime.
// identify() lives on the full Onelo client (Auth + Features bundled) — OneloAuth
// on its own does not expose it. See the Features SDK docs for the call.
② Serverbackend

Install

npm install github:onelo-tools/onelo-node

Initialize

import { Onelo } from '@onelo/node'
import { requireUser } from '@onelo/node/express'

// Authenticate with your SERVER SECRET key (onelo_sk_live_…) — NOT a
// publishable key. Read it from the environment; never hard-code a secret.
const onelo = new Onelo({
  secretKey: process.env.ONELO_SECRET_KEY!,   // onelo_sk_live_…  (NEVER commit)
  apiUrl: 'https://api.onelo.tools',
})

Usage

// The frontend sends the user's ACCESS TOKEN (returned by the Onelo SDK after
// sign-in — NOT your secret key) as: Authorization: Bearer <token>.
// requireUser verifies it and attaches req.oneloUser.
app.get('/me', requireUser(onelo), (req, res) => {
  // requireUser guarantees req.oneloUser is set here — assert non-null.
  res.json({ id: req.oneloUser!.id, email: req.oneloUser!.email })
})

// Post-verify gates + SSE support (all optional):
//   requireUser(onelo, { acceptQueryToken: true })   // allow ?token= for EventSource/SSE
//   requireUser(onelo, { identifyMonitor: false })   // opt out of auto monitor.setUser (on by default when a monitor scope is active)
//   optionalUser(onelo)   // sets req.oneloUser = null instead of 401 (a 503 outage still surfaces)

// ── Other frameworks — one adapter import, same onelo client ──
//   Next.js  → import { withUser } from '@onelo/node/next'
//              export const GET = withUser(onelo, async (req, user) =>
//                Response.json({ id: user.id }))
//   Fastify  → import { requireUser } from '@onelo/node/fastify'
//              fastify.get('/me', { preHandler: requireUser(onelo) },
//                async (req) => ({ id: req.oneloUser!.id }))
//   Hono     → import { requireUser } from '@onelo/node/hono'
//              app.use('/me', requireUser(onelo))
//              app.get('/me', (c) => c.json({ id: c.get('oneloUser').id }))
//   Any other (Koa, Nest, raw http, WebSocket, queue worker):
//              import { verifyToken } from '@onelo/node'
//              const user = await verifyToken(onelo, token)   // throws typed errors
//
// Error → status: OneloAuthInvalidToken / missing → 401 · OneloAuthForbidden /
//   failed gate → 403 · OneloAuthRateLimited (429) / OneloAuthUnavailable (5xx) → 503.
The handoff (frontend + backend): ① the client SDK signs the user in and gets a token → it sends Authorization: Bearer <token> on each request → ② requireUser on your server verifies it against Onelo before serving protected data. Never trust client-supplied identity — always verify server-side.

4Security

🔒Publishable vs secret keys

The client ships a publishable key (safe to embed); only your server holds the secret key. verifyToken / requireUser refuse to run with a publishable key.

🔒Token storage

Keychain (Apple), safeStorage (Electron), EncryptedSharedPreferences (Android) — never plain files or localStorage on native.

🔒Hosted flow

Passwords never touch your app. The one-time code is single-use, short-lived, and bound to your app — it is only delivered to your registered scheme (native) or validated origin (web). OAuth and custom-UI mobile/desktop flows additionally use PKCE.

🔒Server verification

Your backend re-checks every token against Onelo and maps failures to 401 (invalid), 403 (forbidden / failed gate), 503 (Onelo unreachable) — it never trusts client-supplied identity.

5Reference & raw contract

Not using an SDK? Verify a token with a single request — the same endpoint every server SDK calls. Send the user's token as a bearer, and your secret key in the header.

GET https://api.onelo.tools/api/sdk/auth/user
# Authorization: Bearer <user access token>
# X-Onelo-Secret-Key: <your secret key>
# 200 → { id, email, created_at, metadata }
# hosted-auth tokens also include entitlement: "active" | "none" (paid-access status)
# 401 → reject the request

Use your Onelo API base URL (shown on the dashboard SDK tab) — the endpoint lives on the API host, not on your dashboard domain.

Replace onelo_pk_live_YOUR_KEY and the API URL with the values from your dashboard → SDK tab.

Authentication — Onelo Docs