# Fluxul de conectare (/ro/docs/auth/connect-flow)

Pas cu pas — autorizezi cu PKCE, schimbi codul, apelezi /connect/key, stochezi cheia de gateway. Cu o notă despre CORS în browser.

Fluxul de conectare este OpenID Connect obișnuit plus un endpoint codai. Când se termină, deții o cheie de gateway `codai_…` care aparține **utilizatorului tău**, limitată la **consimțământul lui pentru aplicația ta**, și apelezi `https://ai.codai.ro` exact cum descrie [documentația gateway-ului](../gateway/index.mdx).

```text
  aplicația ta ──1 redirect──▶ auth.codai.ro/auth ──login + consimțământ──▶ redirect_uri-ul tău?code=…&state=…
  aplicația ta ──2 POST /token (code + code_verifier)──▶ { access_token, id_token, … }
  aplicația ta ──3 GET /connect/key (Bearer access_token)──▶ { api_key: "codai_…", base_url, … }
  aplicația ta ──4 Authorization: Bearer codai_… ──▶ ai.codai.ro/v1/…   (în numele utilizatorului)
```

<Steps>
<Step>

### Construiești URL-ul de autorizare [#construiești-url-ul-de-autorizare]

PKCE este obligatoriu: generezi un `code_verifier` aleatoriu (43 – 128 caractere URL-safe), derivezi `code_challenge = base64url(sha256(verifier))` și păstrezi verifier-ul plus un `state` aleatoriu în sesiunea utilizatorului.

```text
GET https://auth.codai.ro/auth
  ?client_id=<your client_id>
  &response_type=code
  &redirect_uri=<a registered redirect URI, exact match>
  &scope=openid%20email%20profile%20inference%20keys:manage
  &state=<random>
  &code_challenge=<S256 of the verifier>
  &code_challenge_method=S256
```

Adăugiri opționale: `offline_access` în `scope` pentru un refresh token (doar dacă clientul tău are grant-ul `refresh_token`), `nonce` dacă validezi ID token-urile în modul strict, `acr_values=urn:codai:acr:mfa` pentru a cere un al doilea factor, `resource=https://ai.codai.ro/v1` pentru a fi explicit despre audiență (este și valoarea implicită).

Utilizatorul se autentifică (sau își creează cont) și vede un ecran de consimțământ care listează [scope-urile](./scopes.mdx) cerute de tine. Anularea îl trimite înapoi cu `?error=access_denied&error_description=User%20cancelled%20the%20authorization.`

</Step>
<Step>

### Schimbi codul [#schimbi-codul]

Înapoi la `redirect_uri`-ul tău, verifici `state`, apoi faci `POST /token`:

<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">

```bash
curl https://auth.codai.ro/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=https://app.example.com/callback" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "code_verifier=$VERIFIER"
```

</Tab>
<Tab value="TypeScript">

```ts
const tokenRes = await fetch('https://auth.codai.ro/token', {
  method: 'POST',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://app.example.com/callback',
    client_id: CLIENT_ID,
    code_verifier: verifier,
  }),
});
const tok = (await tokenRes.json()) as { access_token: string; id_token: string; expires_in: number; scope: string; refresh_token?: string };
```

</Tab>
<Tab value="Python">

```python
tok = requests.post(
    "https://auth.codai.ro/token",
    data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": "https://app.example.com/callback",
        "client_id": CLIENT_ID,
        "code_verifier": verifier,
    },
    timeout=15,
).json()
```

</Tab>
</Tabs>

Un client confidențial se autentifică și el aici — `Authorization: Basic base64(client_id:client_secret)` (`client_secret_basic`), `client_secret` în formular (`client_secret_post`) sau un JWT `client_assertion` (`private_key_jwt`). Un client public trimite doar `client_id`.

Răspuns:

```json
{
  "access_token": "opaque…",
  "id_token": "eyJhbGciOiJSUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email profile inference keys:manage",
  "refresh_token": "…only with offline_access…"
}
```

Access token-ul este opac și trăiește o oră. Ai nevoie de el pentru exact un apel în plus.

</Step>
<Step>

### Emiți cheia de gateway [#emiți-cheia-de-gateway]

`GET /connect/key` cu access token-ul. Token-ul trebuie să poarte `keys:manage` **sau** `inference`.

```bash
curl https://auth.codai.ro/connect/key \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{
  "api_key": "codai_xxxxxxxx",
  "api_key_id": "3d6b…-uuid",
  "base_url": "https://ai.codai.ro/v1",
  "model": "codai",
  "already_issued": false
}
```

<TypeTable
  type={{
    api_key: { type: 'string | null', description: 'Cheia de gateway în clar. Returnată exact o dată per consimțământ; null după aceea.' },
    api_key_id: { type: 'string', description: 'Id-ul cheii — ceea ce vede utilizatorul în hub → Keys, etichetat "OAuth · <numele clientului tău>".' },
    base_url: { type: 'string', description: 'URL de bază în stil OpenAI, inclusiv /v1. Îndrepți un SDK OpenAI aici ca atare; elimini /v1 pentru SDK-urile codai, care îl adaugă singure.' },
    model: { type: 'string', description: 'Numele modelului pe care îl trimiți.', default: "'codai'" },
    already_issued: { type: 'boolean', description: 'true când acest consimțământ și-a emis deja cheia și textul în clar nu mai există.' },
  }}
/>

**O cheie per consimțământ.** IdP-ul nu stochează niciodată textul în clar. Dacă apelezi `/connect/key` din nou pentru același grant primești `{ "api_key": null, "already_issued": true, … }` — nu mai e nimic de recuperat. Persistă cheia (criptată) în momentul în care o primești. Ai pierdut-o? Trimite utilizatorul din nou prin pasul de autorizare: un consimțământ nou este un grant nou și emite o cheie nouă, iar utilizatorul o poate revoca pe cea veche din hub.

Grant-urile de consimțământ trăiesc 90 de zile; când unul expiră, utilizatorul se reconectează și tu stochezi cheia nouă.

| Status | `error` | Semnificație |
| --- | --- | --- |
| `401` | `invalid_token` | Fără bearer, sau *Token unknown or expired.* — a trecut o oră sau token-ul a fost revocat. |
| `403` | `insufficient_scope` | Token-ului îi lipsesc atât `keys:manage` cât și `inference`, sau grant-ul nu poartă niciun scope care permite emiterea de chei. |
| `400` | `invalid_grant` | Token-ul nu are un grant de consimțământ în spate (un token în stil client-credentials nu poate emite chei). |

</Step>
<Step>

### Apelezi gateway-ul în numele utilizatorului [#apelezi-gateway-ul-în-numele-utilizatorului]

```ts
import OpenAI from 'openai';

const client = new OpenAI({ apiKey: connected.api_key, baseURL: connected.base_url }); // base_url se termină deja în /v1
const reply = await client.chat.completions.create({ model: connected.model, messages: [{ role: 'user', content: 'Hello from my app' }] });
```

Consumul ajunge pe planul utilizatorului și apare în hub-ul lui sub cheia etichetată cu numele aplicației tale. Trimite [`X-Codai-Client: <your-app>/<version>`](../gateway/headers.mdx#identitatea-clientului) ca lista lui de sarcini să îl atribuie corect.

</Step>
</Steps>

## Apelarea `/connect/key` dintr-un browser [#apelarea-connectkey-dintr-un-browser]

`/connect/key` răspunde la preflight-urile CORS doar pentru o **listă de origini permise**, iar lista este a codai, nu derivată din redirect URI-urile tale. Concret:

- Fără header `Origin` (server-la-server, curl, aplicații native) → cererea funcționează și nu se adaugă niciun header CORS.
- Origine permisă → `Access-Control-Allow-Origin: <origin>`, `Vary: Origin`; preflight `204` cu `Access-Control-Allow-Methods: GET, OPTIONS`, `Access-Control-Allow-Headers: authorization, content-type, x-codai-device-name`, `Access-Control-Max-Age: 86400`.
- Orice altă origine → preflight `403 { "error": "origin_not_allowed" }`; un `GET` direct rulează totuși, dar browser-ul nu poate citi răspunsul.

<Callout type="warn">
O aplicație single-page terță nu poate apela `/connect/key` cross-origin. Fă schimbul de cod **și** emiterea cheii pe backend-ul tău, apoi dă cheia (sau, mai bine, o sesiune legată de ea) browser-ului. `/token` este diferit — CORS-ul lui urmează redirect URI-urile înregistrate pentru clienții publici, așa că un SPA poate schimba codul direct și are nevoie de backend doar pentru `/connect/key`.
</Callout>

Dacă produsul tău are cu adevărat nevoie de un flux doar în browser, cere-ne să adăugăm originea ta în lista permisă la înregistrarea clientului.

## Reîmprospătare și revocare [#reîmprospătare-și-revocare]

- Cu `offline_access`, `POST /token` cu `grant_type=refresh_token&refresh_token=…&client_id=…` returnează un access token nou și — pentru clienții publici întotdeauna, pentru cei confidențiali după 70 % din TTL — un refresh token rotit. *Cheia de gateway nu expiră odată cu access token-ul*; ai nevoie de un access token nou doar dacă vrei să apelezi `/me` sau să re-verifici grant-ul.
- `POST /token/revocation` cu `token=<refresh_token>` încheie sesiunea pe partea ta. Revocarea **grant-ului** (utilizatorul se deconectează din hub) revocă și cheia de gateway; următorul tău apel către `ai.codai.ro` este `401 invalid_api_key` — cere-i utilizatorului să se conecteze din nou.
- `GET /me` cu access token-ul returnează claim-urile pentru scope-urile acordate: `sub`, `email`, `email_verified`, `name`, `picture`, plus `role` și `sid`.
