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.
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
The SDK launches the Onelo sign-in page in a native window (or iframe/popup on web).
Email & password, or OAuth on paid plans — all on Onelo’s page.
A single-use code returns to your app — deep link (your app’s own unique callback scheme, from Settings) on native, postMessage on web.
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.
Install
// Xcode → File → Add Package Dependencies:
// https://github.com/onelo-tools/onelo-swiftInitialize
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.Install
npm install github:onelo-tools/onelo-nodeInitialize
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.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
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.
Keychain (Apple), safeStorage (Electron), EncryptedSharedPreferences (Android) — never plain files or localStorage on native.
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.
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.
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.