Cargo Accounts · One Login · Integration Guide
one.cargo.ac (Cargo Account) Guide
The account home — the ONLY place credentials live. It has real login, signup and reset screens, validates each incoming `return` URL, and bounces every user straight back to the spoke they came from, signed in.
Applies to
one.cargo.acThe ecosystem at a glance
┌──────────────────┐ passwords live ONLY here
│ one.cargo.ac │ login · signup · reset
│ (account home) │◄───────────────┐
└────────┬─────────┘ │
return-redirect │ setSession() │ same-window
(?return=...) ▼ │ handoff
┌──────────────────┐ ┌───────┴────────┐
│ auth.cargo.ac │ │ one.cargo.ac │
│ tokens · JWKS · │◄──────►│ (this spoke) │
│ shared session │ silent │ one.cargo.ac │
└──────────────────┘ probe └────────────────┘
1 button · 1 tab · automatic return — no popup, no manual reopen.The uniform button — both states
Every app renders this exact control. Left: no session yet. Right: silent probe found a session, so one click continues.
No session
Session detected
0. You are the account home — build real auth here
one.cargo.ac is the ONE place credentials live. Unlike every other spoke, you DO build and keep working login, signup and password-reset screens, backed by the shared auth.cargo.ac backend. Every other app sends its users to you and waits for you to send them back.
Shared backend: reached only via https://auth.cargo.ac You render: /login /signup /reset-password (real, functional) No JWKS / Third-Party Auth step — you SHARE the backend, not trust it.
1. Read and validate the incoming `return` URL
Spokes arrive as /login?return=<spoke>/auth/callback. Read that param and validate it against the redirect allowlist (open-redirect guard). Keep it through the whole sign-in so you know exactly where to bounce the user back.
const params = new URL(location.href).searchParams;
const returnUrl = params.get("return");
// MUST be on the allowlist — reject anything else:
if (returnUrl && !isAllowlisted(returnUrl)) throw new Error("bad return URL");2. Sign the user in (real forms, shared backend)
Run your normal email/password and Google flows against the shared backend with signInWithPassword / signInWithOAuth, and signUp on the signup screen. This is the only app allowed to do this.
await authClient.auth.signInWithPassword({ email, password });
// or Google:
await authClient.auth.signInWithOAuth({ provider: "google" });
// signup screen:
await authClient.auth.signUp({ email, password });3. After success, return the user to the spoke (same tab)
The moment sign-in or sign-up succeeds, send the user BACK to the validated return URL in the SAME window. They never close or reopen the spoke — they land there already signed in. If there is no return URL, show your own dashboard.
// SAME-WINDOW bounce back to the originating spoke:
if (returnUrl) {
window.location.assign(returnUrl); // e.g. https://<spoke>/auth/callback
} else {
window.location.assign("/dashboard"); // arrived directly at one.cargo.ac
}4. Signup carries the return through too
New users sent from a spoke land on /signup?return=<spoke>. Carry the return param across the signup → (optional verify) → signed-in steps so brand-new accounts also bounce straight back to the spoke they started on.
// Preserve return across the signup flow:
<a href={"/login?return=" + encodeURIComponent(returnUrl)}>Already have an account?</a>
// After signUp success: window.location.assign(returnUrl ?? "/dashboard");5. Honor global logout
Because you own the shared session, your sign-out must revoke it (and refresh tokens) so every spoke's next silent probe returns login_required. Keep the redirect allowlist in sync below.
https://admin.cargo.ac https://admin.cargo.ac/auth/callback https://api.cargo.app/auth/v1/callback https://api.cargo.directory/auth/v1/callback https://api.cargo.page/auth/v1/callback https://api.cargoworks.ai/auth/v1/callback https://api.cargoworks.io/auth/v1/callback https://auth.cargo.ac https://cargo.ac https://cargo.email https://cargo.email/auth/callback https://cargo.email/auth/silent https://cargodirectory.io https://cargodirectory.io/auth/callback https://cargodirectory.io/auth/silent https://cargoworks.ai https://cargoworks.ai/auth/callback https://cargoworks.ai/auth/silent https://cargoworks.io https://cargoworks.io/auth/callback https://cargoworks.io/auth/silent https://cargoworks.network https://cargoworks.network/auth/callback https://cargoworks.network/auth/silent https://cargoworks.support https://cargoworks.support/auth/callback https://cargoworks.support/auth/silent https://cloud.cargoworks.network https://cloud.cargoworks.network/auth/callback https://cloud.cargoworks.network/auth/silent https://delivery.cargoworks.network https://delivery.cargoworks.network/auth/callback https://delivery.cargoworks.network/auth/silent https://exchange.cargoworks.network https://exchange.cargoworks.network/auth/callback https://exchange.cargoworks.network/auth/silent https://exim.cargoworks.ai https://expert.cargoworks.network https://expert.cargoworks.network/auth/callback https://expert.cargoworks.network/auth/silent https://global.cargo.directory https://id.cargoworks.network https://id.cargoworks.network/auth/callback https://id.cargoworks.network/auth/silent https://mob.cargo.app https://one.cargo.ac https://one.cargo.ac/auth/callback https://track.cargoworks.io https://vault.cargo.page
6. Onboarding a new spoke — it stays generic
When a new client app onboards (e.g. Cargo Page), there is nothing app-specific to build here — one.cargo.ac is GENERIC. Its users simply arrive at one.cargo.ac/login?return=<spoke>/auth/callback and pass through /oauth/consent like every other app. You only VERIFY two things per app: (1) your return-URL allowlist check accepts that spoke's callbacks (production + any preview hosts), and (2) the consent screen renders that spoke's real name and logo, read dynamically from hub data — never hardcoded. Entitlement is NOT decided here: it is enforced at the hub consent step and re-checked by the spoke via the app_access claim, which is empty for everyone until admin.cargo.ac provisions org→app grants — so keep every entitlement check SOFT. Each onboarded client app has its own dedicated tab above with its exact callback, app_access key and a printable verification brief.
// Every spoke arrives the same generic way — no per-app code:
// /login?return=<spoke>/auth/callback
if (returnUrl && !isAllowlisted(returnUrl)) throw new Error("bad return URL");
// Consent screen name + logo come from hub data (do NOT hardcode).
// Entitlement is enforced hub-side; treat empty/missing app_access as
// ALLOW (log only) — never hard sign-out. See each app's tab above.Worked example — a spoke hands a user to you and you return them
A user on exim.cargoworks.ai (no session) clicks "Continue with Cargo Account". They arrive at one.cargo.ac/login?return=https://exim.cargoworks.ai/auth/callback. They sign in. You immediately bounce them back — same tab — to that callback, where exim finishes signed in.
1. exim → window.location.assign(
"https://one.cargo.ac/login?return=" +
encodeURIComponent("https://exim.cargoworks.ai/auth/callback"))
2. one.cargo.ac validates return, shows login form, user signs in.
3. one.cargo.ac → window.location.assign(
"https://exim.cargoworks.ai/auth/callback?code=...")
4. exim exchanges code, sets session, lands on its dashboard. ✅What to delete, keep and add
- KEEP & BUILD: real login, signup and password-reset screens
- KEEP: the shared auth.cargo.ac backend (signInWithPassword / signUp)
- ADD: read + allowlist-validate the `return` URL on every entry
- ADD: same-window bounce back to the spoke after success
- ADD: global logout that revokes the shared session + refresh tokens
- DON'T: add a separate data backend or a JWKS Third-Party Auth issuer
Pitfalls & FAQ
- Should I open the login in a popup or new tab?
- No. Always a SAME-WINDOW, full-page redirect (window.location.assign). The user must never have to close and reopen your app.
- What about the return URL?
- It is REQUIRED on every handoff, must point at your own /auth/callback, and must be on the cargo.ac redirect allowlist. one.cargo.ac rejects any return URL that is not allowlisted.
- Do I need JWKS / Third-Party Auth here?
- No. one.cargo.ac shares the auth.cargo.ac backend, so you use setSession()/the shared client directly. JWKS trust is only for spokes with their OWN backend.
- A new app is onboarding — what do I change in this console?
- Nothing app-specific. one.cargo.ac is generic: it renders any spoke's name and logo on the consent screen from hub data, and validates incoming return URLs against the redirect allowlist. Each onboarded client app gets its own tab on this page with its exact callback, app_access key and a printable verification brief — open that app's tab, run its checklist, and Print / Save as PDF if you want to hand it off. You never hardcode a spoke into the console.
- Why is the app_access claim empty, and who grants access?
- Access is granted in the admin hub (admin.cargo.ac → Organizations → Org App Access), NOT in one.cargo.ac. The app_access claim is the hub's registered apps.key (snake_case). It is legitimately EMPTY for every user right now because the hub has not provisioned any org→app grants yet. The real entitlement gate is the hub consent screen, so keep any spoke/console entitlement check SOFT (log only) and treat empty/missing as allow — never hard sign-out, or you lock everyone out.
8. Test checklist
Confirm each item before flipping production traffic to cargo.ac.
- Real, working login + signup + password-reset screens are present
- Incoming `return` URL is read AND validated against the allowlist
- After sign-in success the user is bounced BACK to the spoke (same tab)
- After signup success new accounts also bounce back to the originating spoke
- No separate data backend / no JWKS Third-Party Auth issuer added
- Global logout revokes the shared session + refresh tokens
- Arriving directly (no return) lands the user on the one.cargo.ac dashboard
- S3/S4 round trip verified: spoke → one.cargo.ac → spoke, signed in, same tab
- Each onboarded client app (see its tab) verified: return URL passes the allowlist and consent shows its real name + logo — and any entitlement check stays SOFT (app_access is empty hub-side; never hard sign-out)
