markdown · 6000 bytes Raw Blame History

Authentication

shithub's API authenticates calls with Personal Access Tokens. PATs are minted via the web UI or, for CLI / non-browser clients, through the device-code grant.

Authorization: Bearer shp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Authorization: token shp_… is accepted as a synonym for tools that hard-code GitHub's older syntax.

Token format

PATs are 40 characters of base32 with the shp_ prefix. They match the regex:

shp_[A-Za-z0-9]{40}

Secret-scanning tools (GitHub's, GitGuardian's, etc.) recognize this prefix.

Failure modes

Status Body Cause
401 {"error":"unauthenticated"} Missing or malformed header.
401 {"error":"invalid token"} Token doesn't exist, was revoked, or has expired.
401 {"error":"account suspended"} The owning account has been suspended by an admin.
403 {"error":"insufficient scope"} Token is valid but lacks the scope this route needs.

Sessions

The web UI uses session cookies, not PATs. Session cookies are not accepted on /api/v1/ — the API is PAT-only. This is a deliberate choice: it keeps CSRF concerns off the API surface and means every API caller is identified by an auditable token.

Creating a token programmatically

Two paths exist:

  1. Web UI — sign in at /login, then mint a token at /settings/tokens. Returns the raw token exactly once.
  2. Device-code grant — the standardised CLI / TV / IoT flow, documented in the next section. The client_id must be in the server's allowlist (default: shithub-cli).

Device-code grant (RFC 8628)

The device-code grant lets a CLI obtain a PAT without prompting the user to paste a token. It mirrors GitHub's /login/device/* shape verbatim.

Endpoints

POST /login/device/code                request a new authorization
POST /login/oauth/access_token         poll until the user approves
GET  /login/device                     browser verification page

/login/device/code and /login/oauth/access_token are CSRF-exempt and accept application/x-www-form-urlencoded bodies.

1. Request a device code

POST /login/device/code
Content-Type: application/x-www-form-urlencoded

client_id=shithub-cli&scope=user%3Aread,repo%3Aread
{
  "device_code":               "f0a1b2c3...",
  "user_code":                 "ABCD-EFGH",
  "verification_uri":          "https://shithub.example/login/device",
  "verification_uri_complete": "https://shithub.example/login/device?user_code=ABCD-EFGH",
  "expires_in": 900,
  "interval":   5
}
  • client_id must be in the server's allowlist. Default: shithub-cli.
  • scope is space- or comma-separated. Omit to receive user:read. Unknown scopes return invalid_scope.
  • device_code is returned once and never echoed back; store it in client memory only.
  • interval is the minimum seconds between polls — see slow_down below.

2. Show the user the code

Open the user's browser to verification_uri_complete (or verification_uri if you can't open URLs with query strings). The user enters the user_code, signs in if needed, then clicks Authorize or Deny.

3. Poll the exchange endpoint

POST /login/oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code
&client_id=shithub-cli
&device_code=f0a1b2c3...

Successful exchange (after the user has approved):

{
  "access_token": "shithub_pat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "token_type":   "bearer",
  "scope":        "user:read,repo:read"
}

The access token is disclosed exactly once. A second exchange of the same device_code returns invalid_grant even after successful approval — clients must cache the token from the first 200 response.

Error codes

All errors are HTTP 400 with a JSON body:

{ "error": "<code>", "error_description": "..." }
error Meaning
authorization_pending User has not approved or denied yet. Keep polling at interval seconds.
slow_down You polled inside the interval window. Increase your delay and retry.
access_denied The user explicitly denied the request. Stop polling.
expired_token The grant outlived its expires_in. Restart from /login/device/code.
invalid_grant device_code unknown or already exchanged.
unauthorized_client client_id is not in the server's allowlist.
invalid_scope One or more requested scopes is not a known shithub scope.
unsupported_grant_type The exchange request used a non-device-code grant_type.
invalid_request Required form fields missing or body malformed.

Lifecycle invariants

  • The device_code is single-use for token disclosure. After a successful exchange the row stays in the database for forensics but further exchanges always return invalid_grant.
  • The user_code is human-typeable: 8 characters from a 32-symbol alphabet that excludes 0/O/1/I, formatted XXXX-XXXX. The verification page also accepts the unhyphenated form.
  • Issued PATs carry the scopes requested at /login/device/code time. The token is named on the user's /settings/tokens page with a recognisable label derived from the CLI's User-Agent.
View source
1 # Authentication
2
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).
6
7 ## Header
8
9 ```
10 Authorization: Bearer shp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
11 ```
12
13 `Authorization: token shp_…` is accepted as a synonym for tools
14 that hard-code GitHub's older syntax.
15
16 ## Token format
17
18 PATs are 40 characters of base32 with the `shp_` prefix. They
19 match the regex:
20
21 ```
22 shp_[A-Za-z0-9]{40}
23 ```
24
25 Secret-scanning tools (GitHub's, GitGuardian's, etc.) recognize
26 this prefix.
27
28 ## Failure modes
29
30 | Status | Body | Cause |
31 |-------:|-----------------------------------|------------------------------------------------------|
32 | 401 | `{"error":"unauthenticated"}` | Missing or malformed header. |
33 | 401 | `{"error":"invalid token"}` | Token doesn't exist, was revoked, or has expired. |
34 | 401 | `{"error":"account suspended"}` | The owning account has been suspended by an admin. |
35 | 403 | `{"error":"insufficient scope"}` | Token is valid but lacks the scope this route needs. |
36
37 ## Sessions
38
39 The web UI uses session cookies, not PATs. Session cookies are
40 **not accepted** on `/api/v1/` — the API is PAT-only. This is a
41 deliberate choice: it keeps CSRF concerns off the API surface
42 and means every API caller is identified by an auditable token.
43
44 ## Creating a token programmatically
45
46 Two paths exist:
47
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`).
53
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`.