Authentication
End-user auth across email + password, magic link, email OTP, TOTP MFA, recovery codes, OIDC / SAML SSO, Sign in with Apple / Google / Microsoft / LinkedIn / Facebook / Discord, passkeys (iOS via ASAuthorization, Android via Credential Manager).
Features
- Email/password, magic link, 6-digit email OTP
- Passkeys / WebAuthn (web, iOS, Android)
- TOTP MFA with recovery codes
- Social: Google, GitHub, Apple, Microsoft, LinkedIn, Facebook, Discord (7 providers, AES-256-GCM encrypted creds at rest)
- OIDC + SAML 2.0 SSO (Business+) with one-click IdP metadata
- SCIM 2.0 user + group provisioning (Business+)
- Bulk hash import: bcrypt, scrypt, argon2 — no forced password reset on migration
- Custom JWT claim templates (Growth+) + signing-key rotation
- Per-IP brute-force limit, anonymous → identified merge
- Session device list with self-service revoke
- HttpOnly-cookie SSR sessions (Next.js / Remix / SvelteKit) via @sendoracloud/sdk-web-ssr — refresh-token rotation w/ reuse detection + CSRF double-submit + edge middleware
Common use cases
- Replace Auth0 / Clerk + their event-stream / CRM / messaging integrations — already bundled.
- B2B SaaS that needs SSO + SCIM + audit log + customer timeline + tiered support routing — one tenant.
- Consumer apps that want anon→identified device-takeover (no duplicate pushes on signin) + lifecycle messaging triggered by signup events out of the box.
Key concepts
- Per-project user pool
- Each project gets its own auth tenant. Same email can sign up across projects independently (Firebase model).
- Identity token
- HMAC of `userId` signed by your backend. Required in strict-identity projects to block client-side spoofing.
- Custom JWT claims
- Growth+ — inject roles / orgs / org-membership into the access token via a template. Customer's backend reads them at the edge.
- OIDC / SAML SSO
- Business+ — Sendora hosts the IdP relay. Per-org metadata XML for one-click IdP import.
Setup
- 1Pick auth flavoursDashboard → Auth Service → Settings. Toggle: email+pass / magic / OTP / MFA / passkeys / OAuth providers.
- 2Configure OAuth providers (optional)Per-provider client id + secret in Settings → Providers. Sendora encrypts `clientSecret` + Apple `.p8` private key at rest.
- 3Custom JWT claims (Growth+)Settings → JWT claims template — `{{ user.role }}` interpolation; preview before save.
End-user auth
Anonymous + upgrade
FREEMint a stable user_id before signup. Upgrade in place when the visitor signs up — same user_id preserved, all attached events / tickets / profile / workflow runs stay bound.
// Anonymous-first
await sendora.auth.signInAnonymously();
// Upgrade in place when visitor signs up
await sendora.auth.signUp({
email: "alice@example.com",
password: "correct-horse-battery-staple",
});Email + password
FREEStandard signup + login. bcrypt by default; passwords never logged.
await sendora.auth.signUp({ email, password });
await sendora.auth.signIn({ email, password });Magic link
FREEOne-tap email link sign-in. Sendora sends the email from auth@sendoracloud.com.
// Send the link
await sendora.auth.sendMagicLink("alice@example.com");
// Later, on the link's landing page:
const user = await sendora.auth.verifyMagicLink(tokenFromUrl);Email OTP (6-digit code)
FREECross-device-friendly alternative to magic link. 5-minute TTL, 5-attempt lockout.
await sendora.auth.sendEmailOtp("alice@example.com");
const user = await sendora.auth.verifyEmailOtp("alice@example.com", "482910");Passkeys (WebAuthn)
FREEFace / Touch ID + Android Credential Manager + WebAuthn. Stored in the user's password manager.
// Register a passkey for the signed-in user
await sendora.auth.passkeys.register({ name: "MacBook Pro" });
// Sign in with passkey
const user = await sendora.auth.passkeys.authenticate({ email });Social sign-in (8 providers)
FREEGoogle, GitHub, Apple, Microsoft, LinkedIn, Facebook, Discord — first-class adapters with verified-email gates. Twitter intentionally rejected (OAuth 2.0 doesn't expose verified email; takeover risk).
// SDK opens the provider's consent screen
await sendora.auth.signInWithGoogle();
await sendora.auth.signInWithGitHub();
await sendora.auth.signInWithApple();
await sendora.auth.signInWithMicrosoft();
await sendora.auth.signInWithLinkedIn();
await sendora.auth.signInWithFacebook();
await sendora.auth.signInWithDiscord();TOTP MFA
FREEGoogle Authenticator-compatible RFC 6238. Users enroll from their account page. Login returns mfaRequired+mfaChallengeToken when enrolled.
// Enroll (Bearer-auth, signed-in user)
const { otpauthUrl, recoveryCodes } = await sendora.auth.enrollMfa();
await sendora.auth.confirmMfa(codeFromAuthenticator);
// Login flow when MFA is on
const r = await sendora.auth.signIn({ email, password });
if (r.mfaRequired) {
await sendora.auth.challengeMfa(r.mfaChallengeToken, codeOrRecovery);
}Password reset + email verification
FREESendora sends the email. SDK exposes 4 helpers for the flow.
await sendora.auth.requestPasswordReset(email);
await sendora.auth.resetPassword(token, newPassword);
await sendora.auth.verifyEmail(token);
await sendora.auth.sendVerificationEmail(); // Bearer-auth, resendEnterprise SSO (OIDC + SAML)
BUSINESS+Per-org OIDC + SAML 2.0. Configure in Authentication → Settings → OIDC SSO. SDK kicks off the redirect flow.
// SDK opens the IdP's authorization page
await sendora.auth.signInWithSso({
returnTo: window.location.href,
});
// On the returnTo page, consume the token from the URL fragment
const user = await sendora.auth.consumeSsoTokenFromUrl();SCIM 2.0 provisioning
BUSINESS+Auto-sync users + groups from your IdP (Okta, Azure AD, JumpCloud, OneLogin). Mint a bearer token in Authentication → Settings → SCIM.
# IdP-side configuration. Paste the bearer token + base URL into your IdP's SCIM connector.
# Base URL: https://api.sendoracloud.com/api/v1/scim/v2 (paste this into IdP's SCIM connector field — IdP appends /Users etc).
# Auth: Bearer scim_…
# Verify the connection from your IdP, OR test with curl:
curl https://api.sendoracloud.com/api/v1/scim/v2/Users \
-H "Authorization: Bearer scim_…" \
-H "Accept: application/scim+json"
curl -X POST https://api.sendoracloud.com/api/v1/scim/v2/Users \
-H "Authorization: Bearer scim_…" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "alice@example.com",
"name": { "givenName": "Alice", "familyName": "Example" },
"active": true,
"emails": [{ "value": "alice@example.com", "primary": true }]
}'JWT verification (your backend)
FREEVerify access tokens server-side using Sendora's per-org JWKS endpoint. RS256, key-rotation safe via kid.
// Node.js / any JWKS-aware verifier (e.g. jose)
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(
new URL("https://api.sendoracloud.com/api/v1/auth-service/<ORG_ID>/.well-known/jwks.json"),
);
const { payload } = await jwtVerify(accessToken, jwks);
// payload.sub, payload.email, payload.is_anonymous, ...your custom claims