# Authentication

Every Livepush endpoint is protected by the `bearerAuth` scheme: a JWT in the
`Authorization` header.

```http
Authorization: Bearer YOUR_API_TOKEN
```

There is no query-parameter or cookie fallback on the resource APIs. A request without a
valid token returns `401`.

Tokens are issued by the auth service at **`https://tokens.livepush.io`**, which is
separate from the resource hosts, through the OAuth 2.0 authorization-code flow.

## Before you start

Register your application at
[the developer console](/console/apps/). You will get:

| | |
| --- | --- |
| `client_id` | Public identifier for your app |
| `client_secret` | Server-side only — never ship it in a browser or mobile bundle |
| Redirect URIs | Where users return after consenting |

A `redirect_uri` you send during the flow must match one of the registered URIs
**exactly**. There is no prefix or wildcard matching, so register every callback you use,
including your local development one.

## Step 1 — Send the user to consent

```
GET https://tokens.livepush.io/oauth2/authorize
      ?client_id=YOUR_CLIENT_ID
      &redirect_uri=https://yourapp.example.com/callback
      &response_type=code
      &scope=streams.read,streams.videos.read
      &state=OPAQUE_RANDOM_STRING
```

`response_type` must be `code`. Scopes may be separated by commas or spaces. Generate
`state` randomly per attempt and store it in the user's session — you verify it in step 3,
and without that check your callback has no CSRF protection.

The user needs an existing Livepush session; this request is authenticated by their `srid`
session cookie, not by a bearer token. If they are not signed in, send them to Livepush to
sign in first and then start the flow.

**This endpoint returns JSON, not an HTML page.** Livepush hands you the data and you build
the consent screen:

```json
{
  "sessionID": "Bk93jd0Ba",
  "app": { "name": "Your App", "avatar": "https://…/icon.png" },
  "scope": [
    { "title": "streams.read", "description": "streams.read" }
  ]
}
```

Show the user which app is asking and which scopes it wants, then collect an approve or
deny. The `sessionID` is valid for **5 minutes** — if the user takes longer, start again.

## Step 2 — Post their decision

```
POST https://tokens.livepush.io/oauth2/authorize
      ?client_id=YOUR_CLIENT_ID
      &redirect_uri=https://yourapp.example.com/callback
      &scope=streams.read,streams.videos.read
      &state=OPAQUE_RANDOM_STRING
```

```json
{
  "session_id": "Bk93jd0Ba",
  "is_rejected": false
}
```

The response is a `301` back to your redirect URI. On approval it carries the grant code:

```
https://yourapp.example.com/callback?code=GRANT_CODE&state=OPAQUE_RANDOM_STRING
```

On refusal or any validation failure it carries an error instead, and no code:

```
https://yourapp.example.com/callback?error=access_denied&error_description=owner+has+denied+access+to+requested+scope
```

## Step 3 — Verify `state`, then exchange the code

Compare the returned `state` against the one you stored. If it does not match, stop — the
callback did not originate from your flow.

```bash
curl -X GET 'https://tokens.livepush.io/oauth2/access_token?grant_type=authorization_code&code=GRANT_CODE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https://yourapp.example.com/callback'
```

`client_id` and `redirect_uri` must match the values the code was issued against, or the
exchange fails with `invalid_request`. The code is **single use** and expires after
**10 minutes**.

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs…",
  "refresh_token": "V1StGXR8_Z5jdHi6B-myT…",
  "token_type": "access_token",
  "expires_in": 1209600
}
```

`expires_in` is **14 days**.

> **Two things to know about this endpoint.** It is a `GET` and it takes the client secret
> as a query parameter, where most OAuth servers use a `POST` body. Query strings end up in
> proxy logs, browser history and server access logs — so call it server-side only, never
> from a browser or mobile client, and make sure your own logging redacts it. Also note
> `token_type` is `access_token` rather than the conventional `Bearer`; send the header as
> `Authorization: Bearer <access_token>` regardless of what this field says.

## Step 4 — Call the API

```bash
curl -X GET 'https://octopus.livepush.io/streams/mine' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

Each endpoint page in this reference names the scope it requires.

## Step 5 — Refresh before day 14

Same endpoint, with `grant_type=refresh_token`, passing the refresh token in the `code`
parameter:

```bash
curl -X GET 'https://tokens.livepush.io/oauth2/access_token?grant_type=refresh_token&code=YOUR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&redirect_uri=https://yourapp.example.com/callback'
```

Refresh tokens are **single use**: the old one is disabled the moment a new pair is issued,
so persist the new `refresh_token` from every response. Reusing a spent one returns
`expired_grant`, and the user has to consent again.

## What is inside a token

Access tokens are JWTs. The payload carries the account and the granted scopes:

```json
{
  "_id": "65f1c2...",
  "id": "us_xxxxxxxxxxxx",
  "ns": "nsXXXXXXXX",
  "name": "Wahaj",
  "scope": ["streams.read", "streams.videos.write"]
}
```

You never need to parse this — but decoding `scope` locally is a cheap way to check what a
token can do before you spend a call finding out.

## Scopes

Ask for the narrowest set that does the job. Every endpoint page names the scope it needs.

| Scope | Grants |
| --- | --- |
| `streams.read` | Read streams and their configuration |
| `streams.write` | Modify streams, including all embed settings |
| `streams.create` | Create streams |
| `streams.delete` | Delete streams |
| `streams.destinations.write` | Add, edit and toggle multistreaming destinations |
| `streams.videos.read` | Read the video library |
| `streams.videos.write` | Upload, edit and delete videos |
| `stream.notifications.read` | Read stream notification settings |
| `stream.notifications.write` | Change stream notification settings |
| `players.read` | Read standalone players |
| `players.write` | Modify players |
| `players.create` | Create players |
| `players.delete` | Delete players |
| `regions.read` | List ingestion and storage regions |
| `root` | Everything — see below |

### `root` bypasses every check

A token carrying `root` passes every scope check without the specific scope being present.
Treat it as an account-wide credential: it can delete streams, recordings and videos.

Scopes containing a colon — `global:deployments`, for instance — are privileged. They are
checked against the account when the token is issued, and a request for one the account
does not hold fails outright.

## Error codes

Returned as `error` on the redirect, or in the body of a failed token exchange.

| Code | Meaning |
| --- | --- |
| `invalid_request` | A required parameter is missing, or the consent session expired |
| `invalid_client` | Unknown `client_id`, unregistered `redirect_uri`, or wrong `client_secret` |
| `invalid_grant` | The code or refresh token is unknown or already used |
| `expired_grant` | The refresh token has been retired |
| `unsupported_response_type` | `response_type` was not `code` |
| `unsupported_grant_type` | `grant_type` was not recognised |
| `invalid_scope` | The scope list was empty or unparseable |
| `access_denied` | The user refused consent |
| `server_error` | The grant could not be recorded — safe to retry once |

## Trick: borrow a token from a logged-in browser tab

When you just want to try an endpoint, standing up the whole OAuth flow is a lot of
ceremony. The dashboard keeps its own access token in `localStorage`, so a working token is
one line away.

Open [app.livepush.io](https://app.livepush.io/) while signed in, open DevTools, and run:

```javascript
copy(localStorage.getItem('access_token'))
```

That puts a bearer token on your clipboard. Paste it straight into the samples on these
pages:

```bash
curl -X GET 'https://squid.livepush.io/account/this' \
  -H "Authorization: Bearer $LIVEPUSH_TOKEN"
```

A few things worth knowing before you rely on it:

- **Assume it can do anything to your account.** The dashboard needs broad access. Decode
  the token and read its `scope` claim if you want to know exactly what you are holding —
  but treat it like your password either way. Never paste it into a shared terminal, a
  ticket, a CI log or a chat message.
- **It is short-lived.** Dashboard tokens expire in about an hour. Re-copy it when calls
  start returning `401`. That makes this fine for exploring and wrong for anything
  scheduled.
- **If you use account switching, read `access_token_alias` instead.** When that key is
  set the dashboard sends it in preference to `access_token`, and it is the one scoped to
  the account you are currently looking at.

Use it to explore. For anything that runs more than once, go through the OAuth flow and
issue a token with the narrow scopes it needs.

## Handling expiry in code

Treat `401` as "get a new token and retry once", never as a permanent failure:

```javascript
async function call(url, init = {}) {
  let response = await fetch(url, withAuth(init));

  if (response.status === 401) {
    await refreshAccessToken();
    response = await fetch(url, withAuth(init));
  }

  if (!response.ok) {
    throw new Error(`Livepush API error ${response.status}`);
  }

  return response.json();
}
```

Do not retry more than once. A `401` immediately after a refresh means the credentials
themselves are wrong, and a loop will just hammer the auth service.

## Keeping tokens safe

- Keep tokens and the client secret server-side. If a browser needs Livepush data, proxy
  the call through your own backend rather than shipping a token to the page.
- Never put `client_secret` in a mobile app or single-page app bundle — it is not a secret
  once it ships.
- Request the narrowest scopes that work, and avoid `root` in anything automated.
- Store refresh tokens encrypted at rest, and replace each one as it is rotated.
