# HomeBinder JWT Verification — Client Guide

## Environment

This endpoint is serving the **tokens.testing.homebinder.com** environment.

| Environment | SSO Issuer | Tokens Issuer | JWKS |
|-------------|-----------|---------------|------|
| Production | `https://sso.homebinder.com` | `https://tokens.homebinder.com` | `https://tokens.homebinder.com/.well-known/jwks.json` |
| Testing | `https://sso.testing.homebinder.com` | `https://tokens.testing.homebinder.com` | `https://tokens.testing.homebinder.com/.well-known/jwks.json` |

**Your client must accept issuers that match the environment it connects to.** A production client should only accept production issuers. A testing client should only accept testing issuers. Never mix them.

## JWKS Endpoints

| URL | Type | Notes |
|-----|------|-------|
| `https://tokens.testing.homebinder.com/.well-known/jwks.json` | JSON (direct) | Cached, CORS-enabled. Recommended for clients. |
| `https://s3.us-east-1.amazonaws.com/homebinder.com/.well-known/jwks/current.json` | JSON (S3 origin) | Canonical source. |

Use the `.well-known` URL as your JWKS endpoint. It follows the standard OIDC discovery convention.

## Algorithm

- **Signing algorithm**: EdDSA (Ed25519)
- **KMS signing algorithm**: `ED25519_SHA_512`
- **Key type in JWKS**: `OKP` with curve `Ed25519`

## Token Format

Standard JWT with three base64url-encoded parts: `header.payload.signature`

### Header

```json
{
  "alg": "EdDSA",
  "typ": "JWT",
  "kid": "<key-id>"
}
```

The `kid` identifies which key signed the token. Match it against the JWKS `kid` field.

### Standard Claims (all tokens)

| Claim | Type | Description |
|-------|------|-------------|
| `iss` | string | Issuer — must be `"https://sso.testing.homebinder.com"` (SSO tokens) or `"https://tokens.testing.homebinder.com"` (M2M tokens) for this environment |
| `sub` | string | Subject — user ID, service name, etc. |
| `aud` | string | Audience — must match your app's expected audience |
| `iat` | number | Issued at (Unix timestamp) |
| `nbf` | number | Not before (Unix timestamp) |
| `exp` | number | Expiration (Unix timestamp) |
| `jti` | string | Unique token ID (UUID) |
| `token_type` | string | `"standard"`, `"extended"`, `"long-lived"`, or `"m2m"` |

### SSO Token Claims

Tokens issued by the SSO gateway (`iss: "https://sso.testing.homebinder.com"`) include:

| Claim | Type | Description |
|-------|------|-------------|
| `email` | string | User's email address |
| `name` | string | Display name |
| `role` | string | App-specific role — `admin`, `viewer`, `user` |
| `scope` | string | Space-separated scopes — e.g. `"read write"` |
| `app_id` | string | The app this token was issued for |

### M2M / Service Token Claims

| Claim | Type | Description |
|-------|------|-------------|
| `client_id` | string | Service identifier |
| `type` | string | `"m2m"` |
| `token_type` | string | `"m2m"` or `"long-lived"` |

## Important: Always Enforce `iss` and `aud`

Every client **must** verify both the `iss` (issuer) and `aud` (audience) claims.

- **`iss`** — must be `https://sso.testing.homebinder.com` (SSO tokens) or `https://tokens.testing.homebinder.com` (M2M tokens) for this environment. **Do not accept issuers from a different environment** (e.g. a production client must reject tokens with a testing issuer).
- **`aud`** — must match your app's audience (see [jwt-architecture.md](https://github.com/InspectionGo/home_binder_gateway/blob/main/docs/jwt-architecture.md) for the full list)

Without these checks, a token issued for a different app or by an untrusted source could be accepted.

All examples below enforce both.

## Verification — Node.js

```bash
npm install jose
```

```javascript
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS_URL = "https://tokens.testing.homebinder.com/.well-known/jwks.json";
const jwks = createRemoteJWKSet(new URL(JWKS_URL));

// Configure for your environment and app:
const ACCEPTED_ISSUERS = ["https://sso.testing.homebinder.com", "https://tokens.testing.homebinder.com"];
const ACCEPTED_AUDIENCE = "https://homebinder.com"; // your app's audience

export async function verifyToken(token) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: ACCEPTED_ISSUERS,
    audience: ACCEPTED_AUDIENCE,
  });
  return payload;
}

// Usage:
// const token = req.headers.authorization?.replace("Bearer ", "");
// const user = await verifyToken(token);
// console.log(user.sub, user.email, user.role, user.scope);
```

## Verification — Python

```bash
pip install PyJWT cryptography requests
```

```python
import jwt

JWKS_URL = "https://tokens.testing.homebinder.com/.well-known/jwks.json"
ACCEPTED_ISSUERS = ["https://sso.testing.homebinder.com", "https://tokens.testing.homebinder.com"]
ACCEPTED_AUDIENCE = "https://homebinder.com"  # your app's audience

jwks_client = jwt.PyJWKClient(JWKS_URL)

def verify_token(token: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(token)
    return jwt.decode(
        token,
        signing_key.key,
        algorithms=["EdDSA"],
        issuer=ACCEPTED_ISSUERS,
        audience=ACCEPTED_AUDIENCE,
    )

# user = verify_token(token)
```

## Verification — Elixir

```elixir
# Add to mix.exs:
# {:jose, "~> 1.11"},
# {:req, "~> 0.5"}

defmodule MyApp.TokenVerifier do
  @jwks_url "https://tokens.testing.homebinder.com/.well-known/jwks.json"
  @accepted_issuers ["https://sso.testing.homebinder.com", "https://tokens.testing.homebinder.com"]
  @accepted_audience "https://homebinder.com"  # your app's audience

  def verify(token) do
    %{body: jwks} = Req.get!(@jwks_url)
    {true, %{fields: claims}, _} =
      jwks
      |> JOSE.JWK.from_map()
      |> JOSE.JWT.verify(token)

    with :ok <- validate_issuer(claims),
         :ok <- validate_audience(claims) do
      {:ok, claims}
    end
  end

  defp validate_issuer(%{"iss" => iss}) when iss in @accepted_issuers, do: :ok
  defp validate_issuer(_), do: {:error, :invalid_issuer}

  defp validate_audience(%{"aud" => aud}) when aud == @accepted_audience, do: :ok
  defp validate_audience(%{"aud" => aud}) when is_list(aud) do
    if @accepted_audience in aud, do: :ok, else: {:error, :invalid_audience}
  end
  defp validate_audience(_), do: {:error, :invalid_audience}
end

# {:ok, claims} = MyApp.TokenVerifier.verify(token)
```

## Key Rotation

Keys are rotated periodically. The JWKS always contains all active public keys. Never hardcode a public key — always verify against the live JWKS. The `jose` library (and equivalents) caches the JWKS automatically and refreshes as needed.

## Token Delivery

Tokens are typically delivered via:
- **Authorization header**: `Authorization: Bearer <token>`
- **URL fragment**: `https://your-app.com/callback#token=<token>` (SSO flow)

URL fragments are never sent to the server — extract client-side with `window.location.hash`.
