# homg SSO — integration guide for apps & AI agents

This document explains how to register a new application against **homg SSO** and authenticate users with OpenID Connect.

- **Issuer:** `https://sso.homg.cz`
- **Discovery:** `https://sso.homg.cz/.well-known/openid-configuration`
- **Docs (HTML):** `https://sso.homg.cz/docs`
- **Docs (Markdown):** `https://sso.homg.cz/docs/integration.md`

SSO is **invite-only**. Users (humans and agents) are created by an admin. There is no public registration.

---

## Identity model

### User types

| Type | Browser login | API access |
|---|---|---|
| `human` | Password (default) or optional email magic link | Optional API keys (`Authorization: Bearer`) |
| `agent` | No password / no UI login | API keys only |

### User fields (minimum)

- `id`, `email` (optional for agents), `username`
- `type` (`human` \| `agent`)
- `avatar_url`, `emoji`, `note`
- `status` (`active` \| `disabled`)

### Apps, roles, clients

- **App** — a product (e.g. `friday`, `sso`). Has `slug`, `name`, `home_url`, `status`.
- **Role** — named bundle of permissions (e.g. `admin`, `member`), assigned to a user **per app**.
- **OAuth client** — OIDC credentials belonging to an app (`client_id`, optional `client_secret`, redirect URIs).

---

## Auth options

### Direct browser login

Humans sign in at `/login`:

- **Password** (default) — email/username + password
- **Email link** (optional) — one-time link sent via [Resend](https://resend.com); requires `SSO_RESEND_API_KEY` and `SSO_MAIL_FROM` on the SSO server

Email-link tokens expire in 15 minutes and are single-use. The login form always shows a generic “check your email” message after a send request (no account enumeration).

### 1) End-user OIDC (recommended for web apps)

Use **Authorization Code + PKCE**.

Endpoints:

| Endpoint | URL |
|---|---|
| Authorize | `https://sso.homg.cz/oauth/authorize` |
| Token | `https://sso.homg.cz/oauth/token` |
| UserInfo | `https://sso.homg.cz/oauth/userinfo` |
| JWKS | `https://sso.homg.cz/oauth/jwks` |
| Logout | `https://sso.homg.cz/logout` |

Supported:

- `response_type=code`
- `grant_types`: `authorization_code`, `refresh_token`
- PKCE: `S256` (required for public clients)
- ID token signed with **RS256**
- **RP-Initiated Logout** on `/logout`:
  - `post_logout_redirect_uri` — where to send the browser after ending the SSO session
  - `client_id` — OAuth client (required with redirect, unless `id_token_hint` is sent)
  - `id_token_hint` — optional; `aud` used as `client_id` when omitted
  - `state` — optional; appended to the redirect URI
  - Redirect is allowed only if the URI matches a registered `redirect_uri`, the app `home_url`, or the **same origin** as either

Example:

```
https://sso.homg.cz/logout
  ?client_id=CLIENT_ID
  &post_logout_redirect_uri=https://my-app.example.com/
  &state=RANDOM
```

Without `post_logout_redirect_uri`, logout still ends the SSO session and redirects to `/login`.

### 2) API keys (humans and agents)

For admin/automation calls to SSO itself:

```http
Authorization: Bearer sso_key_...
```

Useful endpoints:

- `GET /api/me` — current user; optional `?app=slug` scopes `roles` / `permissions` (default `sso`)
- `GET /api/apps` / `POST /api/apps`
- `POST /api/clients`
- `GET /api/users` / `POST /api/users`
- `GET /api/roles` — list role slugs + their permissions
- `GET|POST|PUT|DELETE /api/users/{id}/roles` — read / add / replace / remove app-role assignments
- `GET|POST /api/users/{id}/api-keys` — list / create API keys (plain key returned once on create)
- `POST /api/api-keys/{id}/revoke` / `DELETE /api/api-keys/{id}`

`GET /api/me` only needs a valid API key. Other `/api/*` admin routes require the caller to have the **`admin` role on the `sso` app** (not fine-grained permissions).

---

## Step-by-step: create a new app (for AI agents)

You need an API key for a user that has the **`admin` role on the `sso` app**.

### 1. Create the app

```bash
curl -sS -X POST "https://sso.homg.cz/api/apps" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "my-app",
    "name": "My App",
    "home_url": "https://my-app.example.com",
    "status": "active"
  }'
```

Save the returned `id`.

### 2. Create an OAuth client

**Public SPA / mobile (PKCE, no secret):**

```bash
curl -sS -X POST "https://sso.homg.cz/api/clients" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": 2,
    "name": "My App Web",
    "redirect_uris": ["https://my-app.example.com/oauth/callback"],
    "is_confidential": false
  }'
```

**Confidential server app:**

```bash
curl -sS -X POST "https://sso.homg.cz/api/clients" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": 2,
    "name": "My App Backend",
    "redirect_uris": ["https://my-app.example.com/oauth/callback"],
    "is_confidential": true
  }'
```

Copy `client_id` and `client_secret` (secret shown once).

### 3. Assign users to the app

```bash
# Add one role (does not clear other apps/roles)
curl -sS -X POST "https://sso.homg.cz/api/users/2/roles" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"app":"my-app","role":"member"}'

# Or replace the user's full role set
curl -sS -X PUT "https://sso.homg.cz/api/users/2/roles" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"roles":[{"app":"my-app","role":"member"},{"app":"sso","role":"admin"}]}'
```

You can also assign roles in Admin UI: **Users → edit → App roles**.

Users with an app role see that app on their SSO portal (`home_url` link).

### 3b. Create an API key for an agent (or human)

```bash
curl -sS -X POST "https://sso.homg.cz/api/users/3/api-keys" \
  -H "Authorization: Bearer $SSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"friday-mcp"}'
```

Response includes `api_key` (`sso_key_...`) **once**. Store it for the agent / client app.

Or create users via API (`POST /api/users`) first, then assign roles and keys as above.

### 4. Wire the OIDC login flow

1. Redirect the browser to authorize:

```
https://sso.homg.cz/oauth/authorize
  ?client_id=CLIENT_ID
  &redirect_uri=https://my-app.example.com/oauth/callback
  &response_type=code
  &scope=openid%20profile%20email
  &state=RANDOM
  &nonce=RANDOM
  &code_challenge=BASE64URL_SHA256(verifier)
  &code_challenge_method=S256
```

2. User signs in on SSO (and consents).
3. SSO redirects back with `?code=...&state=...`.
4. Exchange the code:

```bash
curl -sS -X POST "https://sso.homg.cz/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=CODE" \
  -d "redirect_uri=https://my-app.example.com/oauth/callback" \
  -d "client_id=CLIENT_ID" \
  -d "code_verifier=VERIFIER"
# confidential clients also send client_secret
```

5. Validate `id_token` (RS256 via JWKS) and/or call UserInfo with `access_token`.

---

## Claims

ID token / UserInfo include:

| Claim | Meaning |
|---|---|
| `sub` | User id (string) |
| `email` | Email (may be null for agents) |
| `preferred_username` | Username |
| `picture` | Avatar URL |
| `emoji` | Emoji |
| `type` | `human` or `agent` |
| `status` | `active` or `disabled` |
| `locale` | Preferred language (`cs` / `en`) |
| `app` | App slug of the OAuth client (e.g. `friday`) |
| `roles` | Role slugs assigned to the user **for that app** (e.g. `["member"]`) |
| `permissions` | Permission slugs granted via those roles **for that app** |

`app`, `roles`, and `permissions` are scoped to the app that owns the OAuth client used in the login. A user with no role on that app still receives tokens, but `roles` / `permissions` are empty arrays — the client app should treat that as unauthorized if membership is required.

Example UserInfo / ID token profile claims:

```json
{
  "sub": "2",
  "email": "user@example.com",
  "preferred_username": "nicol",
  "picture": null,
  "emoji": "🌋",
  "type": "human",
  "status": "active",
  "locale": "cs",
  "app": "friday",
  "roles": ["member"],
  "permissions": []
}
```

### API key identity (`GET /api/me`)

```http
GET /api/me?app=friday
Authorization: Bearer sso_key_...
```

Returns the same identity fields plus `app`, `roles`, `permissions` for the requested app (default `app=sso`), and `apps` (apps the user can access).

---

## Errors

OAuth token/authorize errors use standard OAuth2 shapes, e.g.:

```json
{"error":"invalid_grant"}
{"error":"invalid_client"}
{"error":"unsupported_grant_type"}
```

JSON admin API errors:

```json
{"error":"unauthorized"}
{"error":"forbidden","permission":"apps.create"}
{"error":"invalid_api_key"}
```

---

## Direct SSO login vs app login

- Visiting `https://sso.homg.cz` directly:
  - **Admin** → Admin CRUD
  - **Non-admin** → personal portal (read-only identity + app links)
- Starting from `/oauth/authorize` continues the OIDC flow after login (does not dump the user on the portal mid-flow).

---

## Checklist for a new integrating app

1. Create `app` with `home_url`
2. Create `oauth_client` with exact redirect URIs
3. Assign users app roles
4. Implement Authorization Code + PKCE
5. Verify ID token signature against `/oauth/jwks`
6. Map `sub` as the stable user id in your app; use `roles` / `permissions` (scoped to your app) for authorization
