@@ -1,7 +1,8 @@ |
| 1 | 1 | # Authentication |
| 2 | 2 | |
| 3 | | -shithub's API is PAT-only. There is no OAuth / device-flow / JWT |
| 4 | | -issuance endpoint today. |
| 3 | +shithub's API authenticates calls with Personal Access Tokens. |
| 4 | +PATs are minted via the web UI or, for CLI / non-browser clients, |
| 5 | +through the [device-code grant](#device-code-grant-rfc-8628). |
| 5 | 6 | |
| 6 | 7 | ## Header |
| 7 | 8 | |
@@ -42,14 +43,120 @@ and means every API caller is identified by an auditable token. |
| 42 | 43 | |
| 43 | 44 | ## Creating a token programmatically |
| 44 | 45 | |
| 45 | | -There is no API for creating PATs; tokens are only created from |
| 46 | | -the web UI. This is intentional — the create-PAT surface is the |
| 47 | | -account's most security-sensitive non-password operation. |
| 46 | +Two paths exist: |
| 48 | 47 | |
| 49 | | -## Future |
| 48 | +1. **Web UI** — sign in at `/login`, then mint a token at |
| 49 | + `/settings/tokens`. Returns the raw token exactly once. |
| 50 | +2. **Device-code grant** — the standardised CLI / TV / IoT flow, |
| 51 | + documented in the next section. The client_id must be in the |
| 52 | + server's allowlist (default: `shithub-cli`). |
| 50 | 53 | |
| 51 | | -OAuth-style application authorizations (`client_id` + `client_ |
| 52 | | -secret`, `code` exchange, refresh tokens) are planned post-MVP. |
| 53 | | -For now, instruct human users to mint a PAT from their settings |
| 54 | | -and supply it to your app via the operator's secret-management |
| 55 | | -flow. |
| 54 | +## Device-code grant (RFC 8628) |
| 55 | + |
| 56 | +The device-code grant lets a CLI obtain a PAT without prompting |
| 57 | +the user to paste a token. It mirrors GitHub's `/login/device/*` |
| 58 | +shape verbatim. |
| 59 | + |
| 60 | +### Endpoints |
| 61 | + |
| 62 | +``` |
| 63 | +POST /login/device/code request a new authorization |
| 64 | +POST /login/oauth/access_token poll until the user approves |
| 65 | +GET /login/device browser verification page |
| 66 | +``` |
| 67 | + |
| 68 | +`/login/device/code` and `/login/oauth/access_token` are CSRF-exempt |
| 69 | +and accept `application/x-www-form-urlencoded` bodies. |
| 70 | + |
| 71 | +### 1. Request a device code |
| 72 | + |
| 73 | +``` |
| 74 | +POST /login/device/code |
| 75 | +Content-Type: application/x-www-form-urlencoded |
| 76 | + |
| 77 | +client_id=shithub-cli&scope=user%3Aread,repo%3Aread |
| 78 | +``` |
| 79 | + |
| 80 | +```json |
| 81 | +{ |
| 82 | + "device_code": "f0a1b2c3...", |
| 83 | + "user_code": "ABCD-EFGH", |
| 84 | + "verification_uri": "https://shithub.example/login/device", |
| 85 | + "verification_uri_complete": "https://shithub.example/login/device?user_code=ABCD-EFGH", |
| 86 | + "expires_in": 900, |
| 87 | + "interval": 5 |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +- `client_id` must be in the server's allowlist. Default: `shithub-cli`. |
| 92 | +- `scope` is space- or comma-separated. Omit to receive |
| 93 | + `user:read`. Unknown scopes return `invalid_scope`. |
| 94 | +- `device_code` is returned once and never echoed back; store it |
| 95 | + in client memory only. |
| 96 | +- `interval` is the minimum seconds between polls — see |
| 97 | + `slow_down` below. |
| 98 | + |
| 99 | +### 2. Show the user the code |
| 100 | + |
| 101 | +Open the user's browser to `verification_uri_complete` (or |
| 102 | +`verification_uri` if you can't open URLs with query strings). |
| 103 | +The user enters the `user_code`, signs in if needed, then clicks |
| 104 | +**Authorize** or **Deny**. |
| 105 | + |
| 106 | +### 3. Poll the exchange endpoint |
| 107 | + |
| 108 | +``` |
| 109 | +POST /login/oauth/access_token |
| 110 | +Content-Type: application/x-www-form-urlencoded |
| 111 | + |
| 112 | +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code |
| 113 | +&client_id=shithub-cli |
| 114 | +&device_code=f0a1b2c3... |
| 115 | +``` |
| 116 | + |
| 117 | +Successful exchange (after the user has approved): |
| 118 | + |
| 119 | +```json |
| 120 | +{ |
| 121 | + "access_token": "shithub_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", |
| 122 | + "token_type": "bearer", |
| 123 | + "scope": "user:read,repo:read" |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +The access token is disclosed **exactly once**. A second exchange |
| 128 | +of the same `device_code` returns `invalid_grant` even after |
| 129 | +successful approval — clients must cache the token from the |
| 130 | +first 200 response. |
| 131 | + |
| 132 | +### Error codes |
| 133 | + |
| 134 | +All errors are HTTP 400 with a JSON body: |
| 135 | + |
| 136 | +```json |
| 137 | +{ "error": "<code>", "error_description": "..." } |
| 138 | +``` |
| 139 | + |
| 140 | +| `error` | Meaning | |
| 141 | +|------------------------|-------------------------------------------------------------------------------| |
| 142 | +| `authorization_pending`| User has not approved or denied yet. Keep polling at `interval` seconds. | |
| 143 | +| `slow_down` | You polled inside the `interval` window. Increase your delay and retry. | |
| 144 | +| `access_denied` | The user explicitly denied the request. Stop polling. | |
| 145 | +| `expired_token` | The grant outlived its `expires_in`. Restart from `/login/device/code`. | |
| 146 | +| `invalid_grant` | `device_code` unknown or already exchanged. | |
| 147 | +| `unauthorized_client` | `client_id` is not in the server's allowlist. | |
| 148 | +| `invalid_scope` | One or more requested scopes is not a known shithub scope. | |
| 149 | +| `unsupported_grant_type`| The exchange request used a non-device-code grant_type. | |
| 150 | +| `invalid_request` | Required form fields missing or body malformed. | |
| 151 | + |
| 152 | +### Lifecycle invariants |
| 153 | + |
| 154 | +- The device_code is single-use for token disclosure. After a |
| 155 | + successful exchange the row stays in the database for forensics |
| 156 | + but further exchanges always return `invalid_grant`. |
| 157 | +- The user_code is human-typeable: 8 characters from a 32-symbol |
| 158 | + alphabet that excludes 0/O/1/I, formatted `XXXX-XXXX`. The |
| 159 | + verification page also accepts the unhyphenated form. |
| 160 | +- Issued PATs carry the scopes requested at `/login/device/code` |
| 161 | + time. The token is named on the user's `/settings/tokens` page |
| 162 | + with a recognisable label derived from the CLI's `User-Agent`. |