└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # The NameDrop Protocol (draft version 0.4.0) 2 | 3 | NameDrop is developed by [TakingNames][0] for delegating control over DNS 4 | domains and subdomains. It is an open protocol, and implementation by others 5 | is encouraged. 6 | 7 | 8 | # Overview 9 | 10 | NameDrop is based on [OAuth2][1], with a few additions to facilitate domain 11 | name delegation. 12 | 13 | One key difference from how OAuth2 is generally implemented, is that client 14 | registration with the authorization server is not required before initiating 15 | grant flows. To maintain a level of security, it is required that the 16 | `client_id` be a prefix string of the `redirect_uri`, ie the `redirect_uri` 17 | (which is where the token ends up) must be on the same domain as the 18 | `client_id`. This method is essentially what is described [here][2]. NameDrop 19 | authorization servers should display the `client_id` to users and inform them 20 | that is who is requesting access. 21 | 22 | All API endpoints described in this article are assumed to be appended to a 23 | base URL. For example, TakingNames uses 24 | 25 | `https://takingnames.io/namedrop` 26 | 27 | It is not necessary for the API name to start with `/namedrop`, but it can 28 | be useful for namespacing if the server has other non-NameDrop endpoints. 29 | 30 | # OAuth2 scopes 31 | 32 | NameDrop scopes are prefixed with `namedrop-`, in order to facilitate 33 | composition with other OAuth2 protocols on the same authorization server. 34 | 35 | The following scopes are currently specified: 36 | 37 | * `namedrop-hosts` - grants control over A, AAAA, CNAME, and ALIAS (aka ANAME) records 38 | * `namedrop-mail` - grants control over MX, DKIM TXT, and SPF TXT records 39 | * `namedrop-acme` - grants control over ACME TXT records 40 | * `namedrop-atproto-handle` - grants control over atproto `did=` TXT records 41 | (see [here](https://atproto.com/specs/handle)) 42 | 43 | Permissions are granted to a FQDN (domain or subdomain). Currently this works 44 | in a hierarchical fashion, ie if you have a token with permissions for 45 | `example.com`, you can create records for any subdomain of `example.com` 46 | (`*.example.com`). Likewise, if you have permissions for `sub.example.com`, you 47 | can create records for `*.sub.example.com`, but not `example.com`. 48 | 49 | # OAuth2 endpoints 50 | 51 | The basic OAuth2 endpoints are defined as follows: 52 | 53 | **`GET /authorize`** 54 | 55 | Authorization endpoint (user consent to get code). Can be a web browser 56 | redirect, or a direct link, such as one printed from a CLI application. 57 | 58 | **`POST /token`** 59 | 60 | Token endpoint (swap code for token). Always server-to-server. 61 | 62 | 63 | # Token 64 | 65 | Access tokens are returned as JSON in the following format: 66 | 67 | ```javascript 68 | { 69 | "access_token": String(), 70 | "refresh_token": String(), 71 | "token_type": "bearer", 72 | "expires_in": Number(), 73 | "permissions": [ 74 | { 75 | "scope": String(), 76 | "domain": String(), 77 | "host": String(), 78 | } 79 | ] 80 | } 81 | ``` 82 | 83 | Example: 84 | 85 | ```json 86 | { 87 | "access_token": "lkjaslkajsoidfnaiosnf", 88 | "refresh_token": "iousdoinfoiseofinsef", 89 | "token_type": "bearer", 90 | "expires_in": 3600, 91 | "permissions": [ 92 | { 93 | "scope": "namedrop-hosts", 94 | "domain": "example.com", 95 | "host": "" 96 | }, 97 | { 98 | "scope": "namedrop-mail", 99 | "domain": "example.com", 100 | "host": "mail" 101 | }, 102 | { 103 | "scope": "namedrop-acme", 104 | "domain": "example.com", 105 | "host": "" 106 | } 107 | ] 108 | } 109 | ``` 110 | 111 | # Getting and setting records 112 | 113 | Records are managed via a simple RPC API. All requests use the POST method 114 | with a JSON body. The `Content-Type` can be anything. This allows browser 115 | clients to send 116 | "[simple](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)" 117 | requests that don't trigger CORS preflights, which are 118 | an abomination. This is safe because all requests are authorized via the 119 | included token property. 120 | 121 | For `create-records`, `set-records`, and `delete-records`, the top-level 122 | `domain` and `host` properties are used as defaults for any records where they 123 | are missing. This information can also be inferred from the token, assuming it 124 | doesn't have permissions for multiple domains/hosts. This can make client code 125 | less verbose. 126 | 127 | `type` is the record type such as `A`, `CNAME`, `MX`, etc. `ttl` and 128 | `priority` are both integers. 129 | 130 | When setting `value` for a record, the template variable `{{host}}` can be 131 | used. It will be replaced with the actual host value when the server evaluates 132 | the record. This is particularly useful for things like DKIM and ACME challenge 133 | records. 134 | 135 | 136 | ## `POST /get-records` 137 | 138 | Retrieves current records. This could be done via DNS, but it's convenient to 139 | provide it as part of the NameDrop API to make things easier for clients. 140 | 141 | The request is JSON in the following format: 142 | 143 | ```javascript 144 | { 145 | "domain": String(), 146 | "host": String(), 147 | "token": String(), 148 | } 149 | ``` 150 | 151 | Example: 152 | 153 | ```json 154 | { 155 | "domain": "example.com", 156 | "host": "sub", 157 | "token": "lkjaslkajsoidfnaiosnf" 158 | } 159 | ``` 160 | 161 | ## `POST /create-records` 162 | 163 | Create new records, returning an error if any duplicate records exist. 164 | 165 | ```javascript 166 | { 167 | "domain": String(), 168 | "host": String(), 169 | "records": [ 170 | { 171 | "domain": String(), 172 | "host": String(), 173 | "type": String(), 174 | "value": String(), 175 | "ttl": Number(), 176 | "priority": Number(), 177 | }, 178 | // more records 179 | } 180 | ``` 181 | 182 | Example: 183 | 184 | ```json 185 | { 186 | "domain": "example.com", 187 | "host": "sub", 188 | "token": "lkjaslkajsoidfnaiosnf", 189 | "records": [ 190 | { 191 | "type": "A", 192 | "value": "192.168.0.1" 193 | }, 194 | { 195 | "host": "sub1._domainkey.{{host}}", 196 | "type": "CNAME", 197 | "value": "f1.example.com.dkim-server.com" 198 | } 199 | ] 200 | } 201 | ``` 202 | 203 | ## `POST /set-records` 204 | 205 | Set records, overriding any existing duplicate records. 206 | 207 | ```javascript 208 | { 209 | "domain": String(), 210 | "host": String(), 211 | "records": [ 212 | { 213 | "domain": String(), 214 | "host": String(), 215 | "type": String(), 216 | "value": String(), 217 | "ttl": Number(), 218 | "priority": Number(), 219 | }, 220 | // more records 221 | } 222 | ``` 223 | 224 | Example: 225 | 226 | ```json 227 | { 228 | "domain": "example.com", 229 | "host": "sub", 230 | "token": "lkjaslkajsoidfnaiosnf", 231 | "records": [ 232 | { 233 | "type": "A", 234 | "value": "192.168.0.1" 235 | }, 236 | { 237 | "host": "sub1._domainkey.{{host}}", 238 | "type": "CNAME", 239 | "value": "f1.example.com.dkim-server.com" 240 | } 241 | ] 242 | } 243 | ``` 244 | 245 | ## `POST /delete-records` 246 | 247 | Delete records, silently ignoring any records that don't exist. 248 | 249 | ```javascript 250 | { 251 | "domain": String(), 252 | "host": String(), 253 | "records": [ 254 | { 255 | "domain": String(), 256 | "host": String(), 257 | "type": String(), 258 | "value": String(), 259 | "ttl": Number(), 260 | "priority": Number(), 261 | }, 262 | // more records 263 | } 264 | ``` 265 | 266 | Example: 267 | 268 | ```json 269 | { 270 | "domain": "example.com", 271 | "host": "sub", 272 | "token": "lkjaslkajsoidfnaiosnf", 273 | "records": [ 274 | { 275 | "type": "A", 276 | "value": "192.168.0.1" 277 | }, 278 | { 279 | "host": "sub1._domainkey.{{host}}", 280 | "type": "CNAME", 281 | "value": "f1.example.com.dkim-server.com" 282 | } 283 | ] 284 | } 285 | ``` 286 | 287 | 288 | 289 | # Other endpoints 290 | 291 | ## `GET /my-ip` 292 | 293 | Returns the public IP of the client, as observed from the server. This is 294 | useful for helping self-hosted clients test whether they can be reached by the 295 | outside world. 296 | 297 | The IP is returned as a simple string. 298 | 299 | 300 | ## `GET /temp-subdomain` 301 | 302 | This causes the server to create a special `A` or `AAAA` record pointing at 303 | the client's IP address, as observed by the server. The created domain is 304 | returned as a simple string. 305 | 306 | The purpose of these domains is to allow the client to retrieve a TLS 307 | certificate from a service like [LetsEncrypt][3], which makes the OAuth2 flows 308 | more secure. This is particularly useful for self-hosters who are trying to 309 | bootstrap a service that doesn't yet have a domain or certificate. 310 | 311 | One way for the server to create these records is by making the subdomain a 312 | representation of the client's IP address. 313 | 314 | For example, if TakingNames received a `/temp-subdomain` request from 315 | `157.245.231.242`, it would create the record and returns something like 316 | this: 317 | 318 | `157-245-231-242.tkip.live` 319 | 320 | The server should ensure the domain remains valid for at least 5 minutes 321 | after a successful request, but no guarantees are required beyond that. 322 | 323 | Server implementors should be aware that free subdomains like this 324 | may be abused for phishing attacks if the records are left around 325 | for too long. 326 | 327 | 328 | 329 | [0]: https://takingnames.io 330 | 331 | [1]: https://oauth.net/2/ 332 | 333 | [2]: https://aaronparecki.com/2018/07/07/7/oauth-for-the-open-web 334 | 335 | [3]: https://letsencrypt.org/ 336 | --------------------------------------------------------------------------------