diff --git a/src/content/docs/build/admin-dashboard.mdx b/src/content/docs/build/admin-dashboard.mdx index 77b39b0..db39b0e 100644 --- a/src/content/docs/build/admin-dashboard.mdx +++ b/src/content/docs/build/admin-dashboard.mdx @@ -2,7 +2,7 @@ title: "Admin Dashboard App" description: "seamless-auth-admin-dashboard is the operator-facing application for managing and observing a Seamless Auth deployment." sidebar: - order: 5 + order: 7 --- # Admin Dashboard diff --git a/src/content/docs/build/custom-auth-ui.mdx b/src/content/docs/build/custom-auth-ui.mdx index 63c5f0d..d0ba09d 100644 --- a/src/content/docs/build/custom-auth-ui.mdx +++ b/src/content/docs/build/custom-auth-ui.mdx @@ -2,7 +2,7 @@ title: "How To Build Your Own Auth UI" description: "Use this guide when the built-in AuthRoutes screens work functionally, but your application needs its own layout, copy, validation, or route structure." sidebar: - order: 6 + order: 8 --- import { Aside } from '@astrojs/starlight/components'; diff --git a/src/content/docs/build/ecosystem.mdx b/src/content/docs/build/ecosystem.mdx index 6bcb515..4173278 100644 --- a/src/content/docs/build/ecosystem.mdx +++ b/src/content/docs/build/ecosystem.mdx @@ -26,8 +26,8 @@ seamless-auth-api @seamless-auth/express or @seamless-auth/core -> connects your backend to the auth engine -@seamless-auth/react - -> connects your frontend to the auth flow +@seamless-auth/react or @seamless-auth/react-native + -> connects your web or mobile frontend to the auth flow seamless-auth-admin-dashboard -> gives operators a UI for users, organizations, sessions, events, security, and config @@ -44,6 +44,8 @@ seamless-auth-admin-dashboard | `@seamless-auth/core` | Framework-agnostic server auth primitives | You are building your own backend adapter | | `@seamless-auth/express` | Official Express adapter | Your backend is Express and you want the supported integration path | | `@seamless-auth/react` | React SDK and built-in auth screens | Your frontend is React and you want a ready-to-use auth integration | +| `@seamless-auth/react-native` | React Native SDK: provider, hooks, native ports | Your frontend is an Expo or React Native app | +| `@seamless-auth/client` | The framework-free client the two SDKs share | You are building a binding for another framework | | `seamless-auth-admin-dashboard` | Operator dashboard app | You want a UI for admins and support teams | | `@seamless-auth/types` | Shared request/response and domain types | You want consistent types across apps and tooling | @@ -105,6 +107,10 @@ with `@seamless-auth/core`. The SDK gives you auth state and flow helpers, but your app still owns routing, authorization, and product-specific UI decisions. +`@seamless-auth/react-native` gives an Expo or React Native app the same state and headless client +over bearer transport, with native passkeys and the platform keystore behind ports. It ships no +screens. See [React Native SDK](/build/react-native-sdk). + --- ### 4. The Admin Dashboard Is A Separate App @@ -160,6 +166,13 @@ Read [React SDK](/build/react-sdk) and --- +### I need to integrate a mobile app + +Read [React Native SDK](/build/react-native-sdk) and [Mobile Passkeys](/build/mobile-passkeys). +`seamless init --mobile=expo` scaffolds an Expo app next to the web and API projects. + +--- + ### I need OAuth, teams, or MFA Read [How To Add OAuth Login](/build/oauth-login), diff --git a/src/content/docs/build/mobile-passkeys.mdx b/src/content/docs/build/mobile-passkeys.mdx new file mode 100644 index 0000000..d744994 --- /dev/null +++ b/src/content/docs/build/mobile-passkeys.mdx @@ -0,0 +1,180 @@ +--- +title: "Mobile Passkeys" +description: "What a native app needs before passkeys and universal links work: an associated domain, the two association files, and the auth server's origins." +sidebar: + order: 6 +--- + +# Mobile Passkeys + +On the web a passkey is bound to the page's origin and the browser checks it. On a phone there is +no page, so the platform checks that the app is allowed to speak for the relying party's domain. +That check is what most of this page sets up. Get it wrong and every ceremony fails with a +generic error, on a real device and in the simulator alike. + +--- + +## The Pieces + +```txt +Relying party domain RPID on the auth server, e.g. app.example.com + | + +-- https://app.example.com/.well-known/apple-app-site-association + +-- https://app.example.com/.well-known/assetlinks.json + | + +-- iOS app entitlement associatedDomains: webcredentials:app.example.com + +-- Android app the signing certificate named in assetlinks.json + | + +-- auth server ORIGINS https://app.example.com and android:apk-key-hash: +``` + +The same association also gives you universal links, so a magic link emailed as +`https://app.example.com/verify-magiclink?token=...` opens the app. + +--- + +## 1. Pick The Domain + +The relying party is the auth server's `RPID`. Use the domain your web app is served from, so web +and native passkeys are the same credentials. The domain must be reachable over HTTPS with a +publicly trusted certificate; both platforms fetch the association files from it, and neither +accepts `localhost`, a private address, or a self-signed certificate. + +For development, use a staging subdomain that hosts the files. Email codes and magic links still +work against a local stack; passkeys do not. + +--- + +## 2. Host The Association Files + +Both files live under `/.well-known/` on the domain, served as `application/json` with no +redirect. + +`apple-app-site-association` (no extension): + +```json +{ + "applinks": { + "details": [ + { + "appIDs": ["TEAMID.com.example.app"], + "components": [{ "/": "/verify-magiclink*" }, { "/": "/oauth/callback*" }] + } + ] + }, + "webcredentials": { "apps": ["TEAMID.com.example.app"] } +} +``` + +`webcredentials` is what passkeys need; `applinks` is what universal links need. The Team ID is +the one on the signing certificate (its `OU`), which is not always the team shown in App Store +Connect for an enterprise or a transferred app. + +`assetlinks.json`: + +```json +[ + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "com.example.app", + "sha256_cert_fingerprints": ["AB:CD:..."] + } + } +] +``` + +`get_login_creds` is what passkeys need; `handle_all_urls` is what app links need. The +fingerprint is the SHA-256 of the signing certificate: your debug keystore's for a development +build, the upload or Play signing key's for a store build. List each one that will sign the app. + +The Expo template ships `tools/associations/generate.mjs`, which writes both files from the team +ID, bundle ID, package name and fingerprint, and prints the Android origin for the next step. + +--- + +## 3. Extend The Auth Server's Origins + +Every passkey ceremony reports the origin it ran in, and the auth server refuses one it does not +know. Add the native origins to `ORIGINS` (or the `origins` system config key), keeping the web +origin first: + +```txt +ORIGINS=https://app.example.com,android:apk-key-hash:q1w2e3... +``` + +- iOS reports the relying party as `https://`, which is usually your web origin already. +- Android reports `android:apk-key-hash:`, where the hash is the base64url encoding of the + same SHA-256 certificate fingerprint, with the colons removed and the bytes decoded. One entry + per signing certificate. +- The web origin stays first. `origins[0]` is the fallback the auth server uses when a request + gives no origin of its own, and an `android:` entry there would break web ceremonies. + +See [System Config Reference](/reference/system-config) for `origins` and `rpid`. + +--- + +## 4. Declare The Association In The App + +Expo (`app.json`): + +```json +{ + "expo": { + "ios": { + "bundleIdentifier": "com.example.app", + "associatedDomains": ["webcredentials:app.example.com", "applinks:app.example.com"] + }, + "android": { + "package": "com.example.app", + "intentFilters": [ + { + "action": "VIEW", + "autoVerify": true, + "data": [{ "scheme": "https", "host": "app.example.com", "pathPrefix": "/verify-magiclink" }], + "category": ["BROWSABLE", "DEFAULT"] + } + ] + } + } +} +``` + +Rebuild the development client after changing either; the entitlement is baked in at build time. + +--- + +## 5. Check It + +- `curl -sI https://app.example.com/.well-known/apple-app-site-association` answers `200` with a + JSON content type and no redirect. Apple fetches through its CDN, so a change can take a while + to reach a device; the simulator fetches directly. +- On a simulator build, the entitlement is in the binary's `__TEXT,__entitlements` section rather + than a provisioning profile. `otool -s __TEXT __entitlements MyApp.app/MyApp` shows it, and is + the quickest way to confirm a rebuild picked it up. +- On Android, `adb shell pm get-app-links com.example.app` shows whether the domain verified. +- A ceremony that fails immediately with a dismissal-shaped error on a device that never showed a + prompt is almost always the association: a stale file, the wrong Team ID, or the wrong + fingerprint for the build you are running. + +--- + +## Rate Limits Behind Carrier NAT + +Phones on mobile networks share a small pool of public addresses. The auth API's per-IP limits on +OTP, magic link and OAuth starts are configurable through the `flow_rate_limits` system config +key, and a mobile audience usually needs them wider than the defaults, which were chosen for +browsers. Per-identity limits stay as they are. + +--- + +## Related Docs + +- [React Native SDK](/build/react-native-sdk) +- [Server SDKs](/build/server-sdks) +- [System Config Reference](/reference/system-config) +- [Environment Variables](/reference/environment-variables) diff --git a/src/content/docs/build/oauth-login.mdx b/src/content/docs/build/oauth-login.mdx index 22c4677..1655ab4 100644 --- a/src/content/docs/build/oauth-login.mdx +++ b/src/content/docs/build/oauth-login.mdx @@ -2,7 +2,7 @@ title: "How To Add OAuth Login" description: "Use this guide when you want users to sign in with an external OAuth provider while still letting Seamless Auth own local users, sessions, cookies, and…" sidebar: - order: 8 + order: 10 --- import { Aside } from '@astrojs/starlight/components'; diff --git a/src/content/docs/build/organizations-memberships.mdx b/src/content/docs/build/organizations-memberships.mdx index 57cf557..ae3222d 100644 --- a/src/content/docs/build/organizations-memberships.mdx +++ b/src/content/docs/build/organizations-memberships.mdx @@ -2,7 +2,7 @@ title: "How To Use Organizations And Memberships" description: "Use this guide when your app needs team, workspace, tenant, or customer-account boundaries." sidebar: - order: 9 + order: 11 --- # How To Use Organizations And Memberships diff --git a/src/content/docs/build/react-native-sdk.mdx b/src/content/docs/build/react-native-sdk.mdx new file mode 100644 index 0000000..7f0b0d1 --- /dev/null +++ b/src/content/docs/build/react-native-sdk.mdx @@ -0,0 +1,250 @@ +--- +title: "React Native SDK" +description: "@seamless-auth/react-native brings Seamless Auth to Expo and React Native apps over bearer transport, with native passkeys and the platform keystore." +sidebar: + order: 5 +--- + +# React Native SDK + +`@seamless-auth/react-native` is the official React Native integration for Seamless Auth. It +gives an Expo or bare React Native app the same session state and headless client the +[React SDK](/build/react-sdk) gives a web app, on top of the pieces a phone needs instead of a +browser: the platform keystore for tokens, the native passkey APIs, and an in-app browser session +for OAuth. + +It is headless. There are no prebuilt screens; you build sign-in the way +[How To Build Your Own Auth UI](/build/custom-auth-ui) describes, with the hooks below. + +--- + +## How It Differs From The Web + +A browser never sees the auth API's tokens: the server adapter keeps them in signed `httpOnly` +cookies. A native app has no cookie jar, so the React Native SDK talks to the same adapter routes +in **bearer transport**: + +```txt +React Native app + -> your backend's /auth adapter routes, with x-seamless-auth-transport: bearer + -> Seamless Auth API +``` + +- The app holds the access and refresh tokens itself, in the keystore, through a + `TokenStoragePort`. +- Each adapter route gets the token it needs in `Authorization: Bearer`: the ephemeral token + `/login` or `/registration/register` returned for the rest of a sign-in flow, the access token + once signed in. +- A 401 on a signed-in call refreshes once through `POST /auth/refresh` and retries. Concurrent + callers share one refresh. A refresh the auth API refuses clears the stored session. +- Your own API routes accept the same access token once `requireAuth` is configured with + `authServerUrl` and `audience`. See [Server SDKs](/build/server-sdks#bearer-transport-for-native-clients). + +The adapter you already run serves both contracts on the same routes. Nothing about the web app +changes. + +--- + +## Installation + +```bash +npx expo install @seamless-auth/react-native expo-secure-store expo-web-browser react-native-passkeys +``` + +The three native modules are peers rather than dependencies, and they are passed into the SDK +rather than imported by it, so Metro never has to resolve a module your app did not install. +Native modules mean a development build (`npx expo run:ios`, `npx expo run:android`); Expo Go +cannot load them. + +`seamless init --mobile=expo` scaffolds all of this alongside the web and API projects. See +[CLI Commands](/reference/cli-commands). + +--- + +## Basic Setup + +```tsx +import * as Passkeys from 'react-native-passkeys' +import * as SecureStore from 'expo-secure-store' +import * as WebBrowser from 'expo-web-browser' +import { + AuthProvider, + createNativePasskeyPort, + createSecureStoreTokenStorage, + createWebBrowserOAuthRedirect, + type NativeAuthPorts, +} from '@seamless-auth/react-native' + +// Built once, at module scope. The provider keys its session on these +// identities, so a fresh object per render would sign the user out. +const ports: NativeAuthPorts = { + passkeys: createNativePasskeyPort(Passkeys), + tokenStorage: createSecureStoreTokenStorage(SecureStore, { key: 'myapp.session' }), + oauthRedirect: createWebBrowserOAuthRedirect(WebBrowser), +} + +export default function App() { + return ( + + + + ) +} +``` + +`apiHost` is your backend's origin, the one that mounts the `/auth` adapter routes, exactly as on +the web. `basePath` changes the mount (default `/auth`). The provider is always in bearer +transport; there is no cookie mode on native. + +### The ports + +| Port | Default | What it wraps | +| -------------- | ---------------------------------------- | --------------------------------------------------------------------------- | +| `passkeys` | none, required | `react-native-passkeys`: ASAuthorization on iOS, Credential Manager on Android | +| `tokenStorage` | memory, which signs out on every restart | `expo-secure-store`: the keychain on iOS, the keystore on Android | +| `oauthRedirect`| none, OAuth disabled | `expo-web-browser`'s `openAuthSessionAsync` | + +Each `create*` helper takes the module as a parameter and returns the port, so a different +library with the same surface can be wrapped the same way. + +--- + +## Reading Auth State + +`useAuth()` returns the same session state and actions as the web SDK: + +```tsx +import { useAuth } from '@seamless-auth/react-native' + +function Gate({ children }) { + const { loading, isAuthenticated, user } = useAuth() + + if (loading) return + if (!isAuthenticated) return + return children +} +``` + +On launch the provider reads the stored tokens and calls `/users/me`, so `loading` stays true +until the session has been restored or refused. Show nothing routable until it settles, or a +signed-in user sees the sign-in screen flash. + +`useAuthClient()` returns the headless client for the multi-step flows (`login`, OTP requests and +verification, `requestMagicLink`, `checkMagicLink`, `verifyMagicLink`, `registerPasskey`, step-up, +organizations). It is the same instance the provider drives; in bearer transport the client holds +the sign-in in flight, so never construct a second one. + +`useLoginMethods()` and `usePasskeySupport()` read what the instance enables and whether this +device can use passkeys, through the passkey port. + +--- + +## Calling Your Own API + +`useAuthorizedFetch()` returns a fetch that carries the access token and refreshes it once on a +401. A path resolves on `apiHost`: + +```tsx +const authorizedFetch = useAuthorizedFetch() +const plan = await authorizedFetch('/api/plan/mine').then(r => r.json()) +``` + +The transport header is not sent to your own routes; they only need the bearer token, which +`requireAuth` reads. A URL under the adapter's mount (`/auth/sessions`, for example) is sent as +an adapter call with the header, so the one fetch reaches every route. + +A 401 that survives the refresh means the session has ended. Treat it as a reason to re-check +the session (`refreshSession()`), not to retry the request. + +--- + +## Sign-In Flows + +The flows are the web's, with the client holding the tokens instead of a cookie: + +```ts +const client = useAuthClient() +const { login, handlePasskeyLogin, markSignedIn, refreshSession } = useAuth() + +// 1. Identifier first. The answer lists what the account can use and hands the +// client the ephemeral token for the rest of the flow. +const { data } = await login(identifier, passkeySupported) + +// 2a. Passkey, when offered and the device supports one. +const passkey = await handlePasskeyLogin() + +// 2b. Or a code. +await client.requestLoginEmailOtp() +await client.verifyLoginEmailOtp(code) + +// 3. The provider loaded its state before the tokens existed. +markSignedIn() +await refreshSession() +``` + +Passkey errors reach you through `getWebAuthnErrorDetail(error)?.name` with the WebAuthn names +(`NotAllowedError` for a dismissed prompt, `InvalidStateError` for a device that already holds +one), whatever the native library threw. + +### Magic links + +A magic link completes wherever it is opened. Poll `client.checkMagicLink()` every few seconds +after `requestMagicLink()`: a `204` (an empty success) means unopened, a body means the session +was issued. Re-check when the app returns to the foreground, since the mail app is where the +link was tapped. + +To open the link in the app rather than the browser, associate your web domain with the app +(universal links on iOS, app links on Android) and route `/verify-magiclink` to a screen that +calls `client.verifyMagicLink(token)` and then `client.checkMagicLink()`. The second call +succeeds only in the app that asked for the link, since that is where the ephemeral token lives; +a cold-started app confirms the link for whichever device is waiting and should say so. +[Mobile Passkeys](/build/mobile-passkeys) covers the association files, which the two features +share. + +### OAuth + +`startOAuthLogin({ providerId, redirectUri })`, then `ports.oauthRedirect.open(authorizationUrl, +redirectUri)`. The in-app browser session closes on `redirectUri` and hands back `{ type: +'callback', code, state }`, which you pass to `finishOAuthLogin`. `redirectUri` must be +registered with the provider and allowed by the auth API: the app's scheme +(`myapp://oauth/callback`) or a universal link. + +--- + +## Passkeys + +`registerPasskey(describeDevice(Platform, 'My iPhone'))` enrols a passkey with a readable name. +`describeDevice` builds the `PasskeyMetadata` from React Native's `Platform` rather than a user +agent string, which a native app does not have. + +Native passkeys work only against a domain the app is associated with. There is no `localhost` +exemption on either platform, so passkeys cannot be tried against a local stack; codes and magic +links can. [Mobile Passkeys](/build/mobile-passkeys) walks through the setup. + +--- + +## Keychain After Reinstall + +iOS keeps keychain items across an uninstall. Without care, a reinstalled app finds the previous +session and signs straight back in, on a phone that may have changed hands. Clear the stored +session on the first launch after install, using a flag in a store that is deleted with the app +(`Settings` on iOS is enough); Android clears its keystore with the app. + +--- + +## Important Boundary + +The React Native SDK is not a backend and does not replace authorization enforcement. Your +backend's `requireAuth` and `requireRole` decide what a token may do; the app only carries it. +Tokens live in the platform keystore and are never logged by the SDK. + +--- + +## Related Docs + +- [Mobile Passkeys](/build/mobile-passkeys) +- [React SDK](/build/react-sdk) +- [Server SDKs](/build/server-sdks) +- [How To Secure Routes](/build/secure-routes) +- [How To Build Your Own Auth UI](/build/custom-auth-ui) +- [Cookie, Session, And Token Reference](/reference/cookies-sessions-tokens) diff --git a/src/content/docs/build/react-sdk.mdx b/src/content/docs/build/react-sdk.mdx index 658ee73..e841313 100644 --- a/src/content/docs/build/react-sdk.mdx +++ b/src/content/docs/build/react-sdk.mdx @@ -30,6 +30,11 @@ Core exports: Important exported types include `AuthContextType`, `User`, `Credential`, `Organization`, `OAuthProvider`, `StepUpStatus`, and the headless client input/result types. +The framework-free parts (the headless client, the session store, the ports and the transport) +live in `@seamless-auth/client`, which `@seamless-auth/react` and +[`@seamless-auth/react-native`](/build/react-native-sdk) both build on and re-export. A React app +never needs to install it directly. + --- ## Choose Your Integration Style @@ -176,6 +181,34 @@ and step-up verification return typed result objects. --- +## Platform Ports And Transport + +The client reaches its platform through two ports, and `AuthProvider` takes them as an optional +`ports` prop: + +- `passkeys` runs the WebAuthn ceremonies. The default is the browser's WebAuthn API through + SimpleWebAuthn. +- `oauthRedirect` opens the provider. The default is a full-page navigation. + +The defaults are what every earlier version did; a React app only reaches for the prop to swap a +port out in tests. The same ports are what +[`@seamless-auth/react-native`](/build/react-native-sdk) fills with the native APIs. + +`transport` chooses how the session travels: `cookie`, the default and the only one a browser +should use, or `bearer`, where the client holds the auth API's tokens through a `tokenStorage` +port and speaks the adapter's bearer contract. Leave it alone on the web. + +`useAuthorizedFetch()` (or `client.authorizedFetch`) is a fetch for your own API that carries the +session the way the transport does: `credentials: 'include'` on the web, the access token with +one refresh-and-retry on native. A path resolves on `apiHost`: + +```ts +const authorizedFetch = useAuthorizedFetch() +const plan = await authorizedFetch('/api/plan/mine').then(r => r.json()) +``` + +--- + ## Custom Auth UI For custom React screens, combine `useAuth()`, `useAuthClient()`, and `usePasskeySupport()`. @@ -390,6 +423,7 @@ The backend layer owns cookie validation, role enforcement, and service-to-servi - [How To Use TOTP And Step-Up](/build/totp-step-up) - [How To Secure Routes](/build/secure-routes) - [Server SDKs](/build/server-sdks) +- [React Native SDK](/build/react-native-sdk) - [Auth API Routes](/reference/auth-api-routes) - [Cookie, Session, And Token Reference](/reference/cookies-sessions-tokens) - [Sessions](/understand/sessions) diff --git a/src/content/docs/build/secure-routes.mdx b/src/content/docs/build/secure-routes.mdx index acec2ae..8bb5141 100644 --- a/src/content/docs/build/secure-routes.mdx +++ b/src/content/docs/build/secure-routes.mdx @@ -2,7 +2,7 @@ title: "How To Secure Routes" description: "Use this guide when you need to protect application pages, API endpoints, or admin-style actions with Seamless Auth." sidebar: - order: 7 + order: 9 --- import { Aside } from '@astrojs/starlight/components'; @@ -241,6 +241,27 @@ if (response.status === 403) { Also confirm the auth API and backend CORS/cookie settings allow the browser origin you are using. For auth API CORS, check `APP_ORIGINS`. For WebAuthn origin checks, check `ORIGINS`. +A native app has no cookies. It carries the auth API's access token instead, and the guard from +Step 2 accepts it once `requireAuth` has `authServerUrl` and `audience`: + +```ts +// backend +app.get( + '/api/admin/report', + requireAuth({ cookieSecret, authServerUrl, audience: authServerUrl }), + requireRole('admin:read'), + handler, +) + +// React Native app +const authorizedFetch = useAuthorizedFetch() +const response = await authorizedFetch('/api/admin/report') +``` + +`authorizedFetch` attaches the token and refreshes it once on a 401. A 401 that survives the +refresh is a session that has ended, not a request to retry. See +[React Native SDK](/build/react-native-sdk). + --- ## Step 7: Use Step-Up For Sensitive Auth Actions @@ -330,6 +351,7 @@ Do not rely on a hidden organization selector in the UI as the only tenant bound - [How To Use TOTP And Step-Up](/build/totp-step-up) - [How To Add OAuth Login](/build/oauth-login) - [Server SDKs](/build/server-sdks) +- [React Native SDK](/build/react-native-sdk) - [React SDK](/build/react-sdk) - [How To Build Your Own Auth UI](/build/custom-auth-ui) - [Users & Roles](/understand/users) diff --git a/src/content/docs/build/server-sdks.mdx b/src/content/docs/build/server-sdks.mdx index 803fe7c..83843ea 100644 --- a/src/content/docs/build/server-sdks.mdx +++ b/src/content/docs/build/server-sdks.mdx @@ -170,6 +170,23 @@ app.get('/api/profile', requireAuth({ cookieSecret }), (req, res) => { This middleware verifies an already-issued access cookie. Session refresh is handled by the adapter's cookie middleware. +With `authServerUrl` and `audience` it also accepts the auth API's own access token in +`Authorization: Bearer`, verified against the auth API's JWKS (issuer, audience, expiry and +`typ: "access"`). This is how a native client authenticates; see +[Bearer Transport For Native Clients](#bearer-transport-for-native-clients). A cookie wins when +both are present, and the two options must be given together. + +```ts +const guard = requireAuth({ + cookieSecret, + authServerUrl: process.env.AUTH_SERVER_URL, + audience: process.env.AUTH_SERVER_URL, +}) +``` + +On the bearer path `req.user.email` and `req.user.phone` are not set, because the access token +does not carry them. `getSeamlessUser(req, options)` loads the profile for either credential. + `requireRole(required)` performs authorization only. It expects `requireAuth` to have populated `req.user`. @@ -192,6 +209,39 @@ Scoped role compatibility: --- +## Bearer Transport For Native Clients + +Browsers get the cookie contract: the adapter holds the auth API's tokens in signed `httpOnly` +cookies and the page never sees them. A native app has no cookie jar, so the same routes serve a +second contract when a request carries the header `x-seamless-auth-transport: bearer`: + +- The client presents the token a route needs in `Authorization: Bearer`. Pre-auth routes (OTP, + passkey login, magic link) take the ephemeral token `/login` or `/registration/register` + returned; signed-in routes take the access token. +- Session-issuing responses come back whole, `token` and `refreshToken` included, and no cookie + is set. The adapter still verifies the auth API's signature on the access token first. +- `POST /auth/refresh` rotates the session: `Authorization: Bearer ` in, a new + pair out. The auth API treats a replayed refresh token as theft and revokes the chain + (`401 { "error": "refresh_token_reused" }`), so a client refreshes once at a time. +- Delivery mode, forwarded client IP and user agent, and the service token behave exactly as in + cookie transport. + +The header selects the transport rather than the presence of `Authorization`, because the first +request of a flow carries no token in either. Requests without the header are unchanged. + +```txt +POST /auth/login -> { token: , loginMethods, ... } +POST /auth/otp/verify-login-email-otp -> { token: , refreshToken, ... } +GET /auth/users/me Authorization: Bearer +POST /auth/refresh Authorization: Bearer +``` + +[`@seamless-auth/react-native`](/build/react-native-sdk) speaks this contract for you; the +[React Native SDK](/build/react-native-sdk) page covers the client side. Your own routes accept +the same access token through `requireAuth` with `authServerUrl` and `audience`, above. + +--- + ## Messaging Delivery The Express adapter can own email and SMS delivery for auth-message flows. @@ -281,7 +331,7 @@ Today, the strongest supported server-side path is Express. The best-supported current combination is: ```txt -@seamless-auth/react +@seamless-auth/react or @seamless-auth/react-native -> your frontend @seamless-auth/express @@ -291,7 +341,8 @@ seamless-auth-api -> your auth engine ``` -Use `@seamless-auth/core` when Express is not your runtime or you are building an adapter. +Use `@seamless-auth/core` when Express is not your runtime or you are building an adapter. The +Fastify adapter, `@seamless-auth/fastify`, mirrors the Express one, bearer transport included. --- @@ -303,6 +354,7 @@ Use `@seamless-auth/core` when Express is not your runtime or you are building a - [How To Use Organizations And Memberships](/build/organizations-memberships) - [How To Use TOTP And Step-Up](/build/totp-step-up) - [React SDK](/build/react-sdk) +- [React Native SDK](/build/react-native-sdk) - [Auth API Server](/build/auth-api) - [Auth API Routes](/reference/auth-api-routes) - [Cookie, Session, And Token Reference](/reference/cookies-sessions-tokens) diff --git a/src/content/docs/build/totp-step-up.mdx b/src/content/docs/build/totp-step-up.mdx index bedfeeb..a51c28b 100644 --- a/src/content/docs/build/totp-step-up.mdx +++ b/src/content/docs/build/totp-step-up.mdx @@ -2,7 +2,7 @@ title: "How To Use TOTP And Step-Up" description: "Use this guide when you need authenticator-app MFA or fresh verification before sensitive actions." sidebar: - order: 10 + order: 12 --- # How To Use TOTP And Step-Up diff --git a/src/content/docs/operate/cli-init.mdx b/src/content/docs/operate/cli-init.mdx index 6908200..da7b3be 100644 --- a/src/content/docs/operate/cli-init.mdx +++ b/src/content/docs/operate/cli-init.mdx @@ -71,6 +71,16 @@ This determines your API server. --- +### Mobile App + +- None (the default) +- Expo (React Native) + +Optional. Picking Expo adds a `mobile/` project on `@seamless-auth/react-native`, signing in +against the same API over bearer transport. Pass `--mobile=expo` to answer without the prompt. + +--- + ### Auth Mode - Docker (recommended) diff --git a/src/content/docs/reference/cli-commands.mdx b/src/content/docs/reference/cli-commands.mdx index ed5edfa..03176b9 100644 --- a/src/content/docs/reference/cli-commands.mdx +++ b/src/content/docs/reference/cli-commands.mdx @@ -64,12 +64,18 @@ existing project was detected and exits; a full existing-project integration flo ### What it does - prompts for frontend and backend choices +- prompts for an optional mobile app (Expo), or takes `--mobile=expo` to skip the prompt - prompts for auth mode - optionally includes the admin dashboard - generates a working local stack - writes `seamless.config.json` - creates `docker-compose.yml` +The mobile app lands in `mobile/` next to the web and API projects, wired to the same API with +`@seamless-auth/react-native`. It is not part of the Docker stack: run it with +`cd mobile && npm install && npx expo run:ios` (or `run:android`). See +[React Native SDK](/build/react-native-sdk). + ### Current generated stack Today the supported generated path is centered on: diff --git a/src/content/docs/reference/system-config.mdx b/src/content/docs/reference/system-config.mdx index 0c89fb6..9152283 100644 --- a/src/content/docs/reference/system-config.mdx +++ b/src/content/docs/reference/system-config.mdx @@ -273,6 +273,11 @@ Production example: `APP_ORIGINS` is separate. It controls CORS for who may call the auth API. `origins` controls WebAuthn verification. +Native apps add their own entries. iOS reports the relying party as `https://`; Android +reports `android:apk-key-hash:`, one per +certificate. Keep the web origin first: `origins[0]` is the fallback for a request that gives no +origin of its own. See [Mobile Passkeys](/build/mobile-passkeys). + --- ## Runtime Cache