# SSO Integration — treating SIMS as your login server

**Audience:** engineers on CRM, Oddit (`ODD-IT`), Todo, or any other
`*.technovative.in` app that authenticates users against SIMS.
**Breaking?** No. Your existing `/api/auth/login`, `/api/auth/verify`, and
JWT-validation code keeps working unchanged — this is additive, a new front
door.

See [`docs/adr/0003-shared-sso-across-subdomains.md`](../../../docs/adr/0003-shared-sso-across-subdomains.md)
for the full design and why it's shaped this way. This document is the
practical "what does my app need to build" guide for CRM, Oddit (`ODD-IT`),
Todo, or any future `*.technovative.in` Consumer App.

Your app never collects a password, never runs a WebAuthn ceremony, and
never talks to SIMS's DB directly. It does two things: redirect
unauthenticated visitors to SIMS, and exchange the code SIMS sends back for
your own tokens.

## 1. What's changing, and why

Previously, a consumer app either showed its own login form and called
`POST /api/auth/login` directly, or — for passkeys — would have had to run
a WebAuthn ceremony on its own origin, which never actually worked: SIMS's
`WEBAUTHN_RP_ORIGIN` was a single hardcoded value that only ever matched
SIMS's own host. Now SIMS is the shared login server for every app on
`*.technovative.in`, which gets you two things you didn't have before:

1. **A working passkey login**, without your app touching WebAuthn at all —
   SIMS hosts the ceremony.
2. **Real single sign-on.** A user who logged into CRM ten minutes ago and
   now opens Todo lands in, already authenticated — no login screen.

## 2. What you need to build

Four things, all on your **backend**:

- [ ] Confirm your app is a registered SIMS `Application` (see §3 — you may
      already be, e.g. CRM's invitation-redirect integration).
- [ ] **Redirect** unauthenticated visitors to SIMS instead of showing your
      own login form.
- [ ] Add a **callback route** (`/sso/callback` or similar) that receives
      `?code=...`.
- [ ] From that route, **exchange the code server-side** for a token pair,
      then set your own app's session exactly as you do today after a
      normal login.

That's it. No new UI, no credential handling, no WebAuthn code.

## 3. Prerequisites

| Item | Value |
|---|---|
| Your `Application.code` | `CRM`, `ODD-IT`, or `TODO` (seeded via `src/utils/seed.py::STANDARD_APPLICATIONS`; also `SIMS`, `LMS`, `IMS`, `NUDGE`) |
| Your origin allowlisted | `https://crm.technovative.in`, `https://oddit.technovative.in`, `https://todo.technovative.in` are already in SIMS's `ALLOWED_CORS_ORIGINS` |
| SIMS base URL | `https://sims.technovative.in` |

The `application` query/body param used below is this `code`, not your
subdomain name. If your app runs on a different origin than the ones
above, ask a SIMS operator to add it — SIMS will reject your `redirect_uri`
otherwise (`redirect_uri_not_allowed`).

## 4. The flow

```mermaid
sequenceDiagram
    participant Browser
    participant YourApp as Your app (backend)
    participant SIMS

    Browser->>YourApp: Visits a page, no local session
    YourApp->>Browser: 302 to SIMS /sso/authorize
    Browser->>SIMS: GET /sso/authorize?redirect_uri=...&application=...

    alt Already has a sims_sso_session cookie
        SIMS->>Browser: 302 back with ?code=... (no login screen)
    else No SIMS session yet
        SIMS->>Browser: Renders SIMS's hosted login page
        Browser->>SIMS: Password / TOTP / passkey
        SIMS->>Browser: 302 back with ?code=...
    end

    Browser->>YourApp: GET /sso/callback?code=...
    YourApp->>SIMS: POST /api/auth/sso/token {code, application}
    SIMS->>YourApp: {access_token, refresh_token}
    YourApp->>YourApp: Validate token, set your own session
    YourApp->>Browser: Redirect into the app, logged in
```

The middle two steps (SIMS's login page, the passkey ceremony) happen
entirely on SIMS's own origin. Your app never sees a password, a passkey,
or a WebAuthn API call.

## 5. Redirect unauthenticated visitors to SIMS

Wherever your app currently shows a login screen, redirect instead:

```
https://sims.technovative.in/sso/authorize
  ?redirect_uri=https://crm.technovative.in/sso/callback
  &application=CRM
```

- `redirect_uri` must **exactly** match one of the origins in SIMS's
  `ALLOWED_CORS_ORIGINS` — the same allowlist that already gates CORS and
  invitation links. Ask a SIMS operator to add your origin if it's missing;
  SIMS will reject the request with `redirect_uri_not_allowed` otherwise.
- `application` is your `Application.code` from §3.

If the visitor already has a live SIMS session (they logged into another
`*.technovative.in` app recently), they'll be bounced straight back with a
code — no login screen, no user action. Otherwise they'll see SIMS's hosted
login page (password, TOTP, passkey) and land back at your `redirect_uri`
once they finish.

## 6. Handle the callback — server-to-server code exchange

Your `/sso/callback` route receives `?code=...`. From your **backend**
(never client-side JS — the code is meant to be redeemed exactly once, by
you, not exposed further), exchange it immediately:

```python
import requests

SIMS_BASE_URL = "https://sims.technovative.in"

def handle_sso_callback(code: str) -> dict:
    resp = requests.post(
        f"{SIMS_BASE_URL}/api/auth/sso/token",
        json={"code": code, "application": "CRM"},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["data"]  # {"access_token": "...", "refresh_token": "..."}
```

The code is single-use and expires after 60 seconds — exchange it
immediately on receipt, don't defer or retry with the same value. A stale or
already-used code returns `400 code_invalid`.

## 7. Validate the token, set your own session

The returned `access_token` carries `aud: "CRM"` (or your own code) and the
user's full `permissions`/`scope` claims, exactly like a normal SIMS login —
validate it via `POST /api/auth/verify?application=CRM` the same way your
app already does (see [`crm_example.py`](crm_example.py)/[`lms_example.py`](lms_example.py)):

```python
def verify_and_establish_session(access_token: str):
    resp = requests.post(
        f"{SIMS_BASE_URL}/api/auth/verify",
        headers={"Authorization": f"Bearer {access_token}"},
        params={"application": "CRM"},
        timeout=10,
    )
    resp.raise_for_status()
    claims = resp.json()["data"]
    # ... set your own cookie/session using claims["sub"], claims["scope"], etc.
```

What you do after that — cookie, server-side session, whatever your app
already uses — is entirely your own concern; SIMS's job ends at handing you
a valid token pair.

## 8. API reference

### `GET /sso/authorize`

Browser-navigated, not a JSON API call.

| Param | Required | Notes |
|---|---|---|
| `redirect_uri` | yes | Must exactly match an allowlisted origin |
| `application` | yes | Your `Application.code` |

| Outcome | Response |
|---|---|
| Already has a valid SIMS session | `302` → `{redirect_uri}?code={code}` |
| No session yet | `200`, renders SIMS's hosted login page |
| Bad `redirect_uri` | `400 redirect_uri_not_allowed` |
| Unknown `application` | `404 application_not_found` |

### `POST /api/auth/sso/token`

Server-to-server only.

**Request**

```json
{ "code": "...", "application": "CRM" }
```

**Response `200`**

```json
{ "success": true, "data": { "access_token": "...", "refresh_token": "..." } }
```

**Errors**

| Status | `error_code` | Meaning |
|---|---|---|
| `400` | `code_invalid` | Unknown, expired (>60s old), or already redeemed |
| `400` | `application_mismatch` | Code was issued for a different `application` than you sent |

## 9. Testing checklist

- [ ] **Cold visit** — no SIMS session anywhere → redirected to SIMS, see
      the login page, complete login, land back in your app authenticated.
- [ ] **Warm visit** — already logged into another `*.technovative.in` app
      → redirected to SIMS and immediately bounced back with a code, no
      login screen shown.
- [ ] **Passkey login** — works via SIMS's hosted page (nothing to build on
      your side, but worth confirming end-to-end).
- [ ] **Code replay** — reuse the same `code` twice → second attempt gets
      `code_invalid`.
- [ ] **Expired code** — wait 60+ seconds before exchanging →
      `code_invalid`.
- [ ] **Wrong `application`** — exchange a code issued for a different
      app's code → `application_mismatch`.
- [ ] **Logout on SIMS** — log out from SIMS's own portal (or another
      consumer app once it supports it), then revisit your app cold →
      shows the login page again, not a silent bounce.

## 10. FAQ

**Do we lose the ability to call `/api/auth/login` directly?**
No — nothing about it changed. Useful for mobile clients or anywhere a
redirect flow doesn't fit. SSO is the recommended path for browser-based
flows specifically.

**What if a user logs out of our app but not SIMS?**
Expected. SSO session state lives on SIMS; logging out locally only ends
your app's own session. The user will be silently re-authenticated if they
come back — same as any real SSO provider.

**Can we still run our own login form as a fallback?**
You can, but there's no reason to — SIMS's hosted page already handles
password, TOTP, and passkey, and keeping a second credential-entry surface
around means users can bypass whichever one has weaker protections.
Recommended: remove your own login form once this is wired up.

**Where do we register our app / rotate credentials / add scopes?**
Application registration is handled by the SIMS team via
`src/utils/seed.py::STANDARD_APPLICATIONS` — reach out if you need a new
code or your origin isn't allowlisted yet.

## What SIMS handles vs. what you build

| | SIMS | Your app |
|---|---|---|
| Login UI, password/TOTP/passkey | ✅ | — |
| WebAuthn ceremony | ✅ (always on SIMS's own origin) | — |
| Recognizing a returning, already-logged-in user | ✅ (`sims_sso_session` cookie) | — |
| `/sso/callback` route | — | ✅ |
| Code → token exchange (server-to-server) | ✅ endpoint | ✅ caller |
| Your own app session after that | — | ✅ |
