# UAuth — OIDC Integration Guide

> This guide is for **developers / AI agents** building a relying party (your app) that connects to UAuth.
> UAuth is a standard **OpenID Connect (OIDC) provider** — use any OIDC client library,
> point it at the discovery URL, and run a normal authorization_code flow.

---

## 1. Overview

UAuth is a central identity provider that wraps social login (Google/Facebook/LINE/Discord).
Your app talks to **UAuth only** over OIDC — it never needs to know about the social providers behind it.

```
your app ──(OIDC authorization_code + PKCE)──► UAuth ──► Google/Facebook/LINE/Discord
   ▲                                            │
   └──────────── id_token + access_token ───────┘
```

- **Supported flow:** `authorization_code` + **PKCE (S256)** + `refresh_token`
- **id_token:** a JWT signed with **RS256** (verify it with the public key from JWKS)
- **access_token:** opaque (call `/userinfo` to read claims)

---

## 2. Endpoints

Base / **issuer**: `https://uauth.u2.skin`

| Purpose | URL |
|---|---|
| **Discovery** (start here) | `https://uauth.u2.skin/.well-known/openid-configuration` |
| JWKS (public key) | `https://uauth.u2.skin/.well-known/jwks.json` |
| Authorization | `https://uauth.u2.skin/authorize` |
| Token | `https://uauth.u2.skin/token` |
| UserInfo | `https://uauth.u2.skin/userinfo` |

> **Recommended:** use an OIDC library that reads discovery automatically (e.g. `openid-client`, NextAuth/Auth.js,
> `coreos/go-oidc`) and set `issuer = https://uauth.u2.skin` — the library will find the endpoints for you.

The real discovery document:
```json
{
  "issuer": "https://uauth.u2.skin",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "scopes_supported": ["openid", "profile", "email", "offline_access"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"]
}
```

---

## 3. Register a client (in the UAuth admin)

> **Before you can do this, you need a developer account.** Open `https://uauth.u2.skin/admin/register`,
> sign up with an allowed email, then log in to the admin.

Go to `https://uauth.u2.skin/admin/clients` → **create a new client**.

There are two kinds:

### a) Public client (SPA / mobile / frontend-only) — recommended with PKCE
- Tick **public client** when creating → you get a `client_id` (no secret)
- **PKCE is required**
- `token_endpoint_auth_method = none`

### b) Confidential client (has a backend server that can keep a secret)
- Leave public unticked → you get a `client_id` + `client_secret` (**shown once — save it now**)
- Send the secret at `/token` using `client_secret_post` or `client_secret_basic`

### Redirect URI (important — security)
On the client detail page → add your app's **redirect URI**, e.g. `http://localhost:3000/api/auth/callback`
- Validated by **exact match** (scheme/host/port/path must match exactly) — **no wildcards**
- **dev:** allows `http://localhost:<port>` and `https://`
- **prod:** requires `https://` only, no localhost

---

## 4. Authorization Code + PKCE flow (step by step)

### Step 0 — create PKCE + state (in your app)
```
code_verifier  = random 43–128 chars (base64url)
code_challenge = BASE64URL(SHA256(code_verifier))
state          = random ≥ 8 chars     ⚠️ must be ≥ 8 long (otherwise UAuth rejects it: "invalid_state")
nonce          = random (recommended)
```
Store `code_verifier`, `state`, and `nonce` in your app's session/cookie.

### Step 1 — redirect the user to `/authorize`
```
GET https://uauth.u2.skin/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=<REDIRECT_URI>        # must exactly match the registered one
  &scope=openid%20profile%20email%20offline_access
  &state=<STATE>
  &nonce=<NONCE>
  &code_challenge=<CODE_CHALLENGE>
  &code_challenge_method=S256
```
UAuth has the user log in (social) and consent, then **303 redirects** back to:
```
<REDIRECT_URI>?code=<AUTH_CODE>&scope=...&state=<STATE>
```
> Check that the returned `state` == the state you sent (CSRF protection).

### Step 2 — exchange the code at `/token`
```
POST https://uauth.u2.skin/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<AUTH_CODE>
&redirect_uri=<REDIRECT_URI>          # must match the one from /authorize
&client_id=<CLIENT_ID>
&code_verifier=<CODE_VERIFIER>
# confidential client: also send client_secret=<SECRET> (or use HTTP Basic)
```
Response:
```json
{
  "access_token": "ory_at_...",
  "token_type": "bearer",
  "expires_in": 3600,
  "id_token": "eyJ...",          // JWT (RS256)
  "refresh_token": "ory_rt_...", // only when you request the offline_access scope
  "scope": "openid profile email offline_access"
}
```

### Step 3 — use / verify the tokens
- **id_token** = the user's identity (see §5) — verify its signature with JWKS
- **access_token** = an opaque token for calling `/userinfo`

```
GET https://uauth.u2.skin/userinfo
Authorization: Bearer <ACCESS_TOKEN>
```
Response (only the claims allowed by the granted scopes):
```json
{
  "sub": "<user uuid>",
  "email": "user@example.com",
  "email_verified": true,
  "name": "User Name",
  "picture": "https://.../avatar.png"
}
```

---

## 5. id_token (claims + verification)

An example decoded payload:
```json
{
  "iss": "https://uauth.u2.skin",
  "sub": "00000000-0000-0000-0000-000000000001",   // = users.id (stable per user)
  "aud": ["<CLIENT_ID>"],
  "exp": 1780128076,
  "iat": 1780124476,
  "auth_time": 1780124476,
  "nonce": "<NONCE>",
  "email": "user@example.com",
  "email_verified": true,
  "name": "User Name",
  "picture": "https://.../avatar.png"
}
```

**How to verify (required):**
1. RS256 signature with the public key from `jwks_uri` — pick the key by the `kid` in the JWT header
2. `iss` == `https://uauth.u2.skin`
3. `aud` contains your `client_id`
4. `exp` is not expired
5. `nonce` == the one you sent

> **`sub` is the user's primary identifier** (= `users.id` in UAuth) — use it to link the user in your app.
> Don't use email as the key: some providers (LINE/Facebook) may not return one, so `email` can be `null`/missing.

---

## 6. Refresh token

Request the `offline_access` scope and you'll also receive a `refresh_token`. Renew with:
```
POST /token
grant_type=refresh_token
&refresh_token=<REFRESH_TOKEN>
&client_id=<CLIENT_ID>
# confidential: + client_secret
```
> The refresh token **rotates** every time — the old one stops working, so store the new one you get back.

---

## 7. Scopes

| scope | what you get |
|---|---|
| `openid` | **required** — enables OIDC, returns an id_token |
| `profile` | `name`, `picture` |
| `email` | `email`, `email_verified` |
| `offline_access` | a `refresh_token` |

---

## 8. Examples

### Next.js (Auth.js / NextAuth v5) — custom OIDC provider
```ts
// auth.ts
import NextAuth from "next-auth"

export const { handlers, auth } = NextAuth({
  providers: [{
    id: "uauth",
    name: "UAuth",
    type: "oidc",
    issuer: "https://uauth.u2.skin",
    clientId: process.env.UAUTH_CLIENT_ID,
    clientSecret: process.env.UAUTH_CLIENT_SECRET, // public client: leave empty + use PKCE
    authorization: { params: { scope: "openid profile email offline_access" } },
    checks: ["pkce", "state", "nonce"],
  }],
})
```
Redirect URI to add in the UAuth admin:
`http://localhost:3000/api/auth/callback/uauth`

### Go (coreos/go-oidc + x/oauth2)
```go
provider, _ := oidc.NewProvider(ctx, "https://uauth.u2.skin")
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
oauth2Cfg := &oauth2.Config{
    ClientID:     clientID,
    ClientSecret: clientSecret,
    Endpoint:     provider.Endpoint(),
    RedirectURL:  "http://localhost:3000/callback",
    Scopes:       []string{oidc.ScopeOpenID, "profile", "email", "offline_access"},
}
// use oauth2.GenerateVerifier() / S256ChallengeOption() for PKCE
```

### curl (test token/userinfo once you have a code)
See `scripts/oidc_flow.sh` in the UAuth repo.

---

## 9. Gotchas (real ones — read before you build)

- ⚠️ **`state` must be ≥ 8 characters**, otherwise `/authorize` returns `invalid_state` (UAuth enforces a minimum entropy)
- ⚠️ **`redirect_uri` must match exactly** what's registered (including scheme/host/port/path) — no wildcards
- ⚠️ **PKCE is required for public clients** — and it must be `S256` (`plain` is not accepted)
- ⚠️ **dev** allows `http://localhost:<port>` redirects · **prod** requires `https://` only
- `/authorize` redirects with **303 See Other** (not 302)
- An `authorization_code` works **once** — reusing it revokes every token in that grant
- `email` may be **null/missing** (LINE/Facebook) → don't rely on email as a key, use `sub`
- The access_token is **opaque** (not a JWT) → read claims only via `/userinfo`

---

## 10. Error reference (OAuth2 standard)

| error | cause |
|---|---|
| `invalid_state` | state is shorter than 8 chars |
| `invalid_redirect_uri` | redirect_uri doesn't match what's registered / violates policy (remote http / wildcard / localhost in prod) |
| `invalid_client` | wrong client_id/secret |
| `invalid_grant` | code expired/already used, or the PKCE verifier doesn't match |
| `access_denied` | user declined consent, or the account is banned |
| `invalid_scope` | requested a scope the client isn't allowed to use |

---

## 11. Checklist for a new app

- [ ] Create a client at `/admin/clients` (choose public/confidential)
- [ ] Add your app's redirect URI on the client detail page
- [ ] Set your app's env: `issuer=https://uauth.u2.skin`, `client_id`, (`client_secret` if confidential)
- [ ] Request scopes `openid profile email` (+ `offline_access` if you need refresh)
- [ ] Do PKCE + state (≥8) + nonce
- [ ] Verify the id_token (sig/iss/aud/exp/nonce) and link the user by `sub`
