Documentation for Token based Auth
Outlines the format of the tokens and how they are verified. Outlines how clients should respond to bearer token authorization challenges. Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)master
							parent
							
								
									fe20d2c38f
								
							
						
					
					
						commit
						a46af29783
					
				|  | @ -0,0 +1,413 @@ | |||
| # Docker Registry v2 authentication via central service | ||||
| 
 | ||||
| Today a Docker Registry can run in standalone mode in which there are no | ||||
| authorization checks. While adding your own HTTP authorization requirements in | ||||
| a proxy placed between the client and the registry can give you greater access | ||||
| control, we'd like a native authorization mechanism that's public key based | ||||
| with access control lists managed separately with the ability to have fine | ||||
| granularity in access control on a by-key, by-user, by-namespace, and | ||||
| by-repository basis. In v1 this can be configured by specifying an | ||||
| `index_endpoint` in the registry's config. Clients present tokens generated by | ||||
| the index and tokens are validated on-line by the registry with every request. | ||||
| This results in a complex authentication and authorization loop that occurs | ||||
| with every registry operation. Some people are very familiar with this image: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| The above image outlines the 6-step process in accessing the Official Docker | ||||
| Registry. | ||||
| 
 | ||||
| 1. Contact the Docker Hub to know where I should download “samalba/busybox” | ||||
| 2. Docker Hub replies: | ||||
|     a. samalba/busybox is on Registry A | ||||
|     b. here are the checksums for samalba/busybox (for all layers) | ||||
|     c. token | ||||
| 3. Contact Registry A to receive the layers for samalba/busybox (all of them to | ||||
|    the base image). Registry A is authoritative for “samalba/busybox” but keeps | ||||
|    a copy of all inherited layers and serve them all from the same location. | ||||
| 4. Registry contacts Docker Hub to verify if token/user is allowed to download | ||||
|    images. | ||||
| 5. Docker Hub returns true/false lettings registry know if it should proceed or | ||||
|    error out. | ||||
| 6. Get the payload for all layers. | ||||
| 
 | ||||
| The goal of this document is to outline a way to eliminate steps 4 and 5 from | ||||
| the above process by using cryptographically signed tokens and no longer | ||||
| require the client to authenticate each request with a username and password | ||||
| stored locally in plain text. | ||||
| 
 | ||||
| The new registry workflow is more like this: | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| 1. Attempt to begin a push/pull operation with the registry. | ||||
| 2. If the registry requires authorization it will return a `401 Unauthorized` | ||||
|    HTTP response with information on how to authenticate. | ||||
| 3. The registry client makes a request to the authorization service for a | ||||
|    signed JSON Web Token. | ||||
| 4. The authorization service returns a token. | ||||
| 5. The client retries the original request with the token embedded in the | ||||
|    request header. | ||||
| 6. The Registry authorizes the client and begins the push/pull session as | ||||
|    usual.  | ||||
| 
 | ||||
| ## Requirements | ||||
| 
 | ||||
| - Registry Clients capable of generating key pairs which can be used to | ||||
|   authenticate to an authorization server. | ||||
| - An authorization server capable of managing user accounts, their public keys, | ||||
|   and access controls to their resources hosted by any given service (such as | ||||
|   repositories in a Docker Registry). | ||||
| - A Docker Registry capable of trusting the authorization server to sign tokens | ||||
|   which clients can use for authorization and the ability to verify these | ||||
|   tokens for single use or for use during a sufficiently short period of time. | ||||
| 
 | ||||
| ## Authorization Server Endpoint Descriptions | ||||
| 
 | ||||
| This document borrows heavily from the [JSON Web Token Draft Spec](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32) | ||||
| 
 | ||||
| The described server is meant to serve as a user account and key manager and a | ||||
| centralized access control list for resources hosted by other services which | ||||
| wish to authenticate and manage authorizations using this services accounts and | ||||
| their public keys. | ||||
| 
 | ||||
| Such a service could be used by the official docker registry to authenticate | ||||
| clients and verify their authorization to docker image repositories. | ||||
| 
 | ||||
| Docker will need to be updated to interact with an authorization server to get | ||||
| an authorization token. | ||||
| 
 | ||||
| ## How to authenticate | ||||
| 
 | ||||
| Today, registry clients first contact the index to initiate a push or pull. | ||||
| For v2, clients should contact the registry first. If the registry server | ||||
| requires authentication it will return a `401 Unauthorized` response with a | ||||
| `WWW-Authenticate` header detailing how to authenticate to this registry. | ||||
| 
 | ||||
| For example, say I (username `jlhawn`) am attempting to push an image to the | ||||
| repository `samalba/my-app`. For the registry to authorize this, I either need | ||||
| `push` access to the `samalba/my-app` repository or `push` access to the whole | ||||
| `samalba` namespace in general. The registry will first return this response: | ||||
| 
 | ||||
| ``` | ||||
| HTTP/1.1 401 Unauthorized | ||||
| WWW-Authenticate: Bearer realm="https://auth.docker.com/v2/token/",service="registry.docker.com",scope="repository:samalba/my-app:push" | ||||
| ``` | ||||
| 
 | ||||
| This format is documented in [Section 3 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-3) | ||||
| 
 | ||||
| The client will then know to make a `GET` request to the URL | ||||
| `https://auth.docker.com/v2/token/` using the `service` and `scope` values from | ||||
| the `WWW-Authenticate` header. | ||||
| 
 | ||||
| ## Requesting a Token | ||||
| 
 | ||||
| #### Query Parameters | ||||
| 
 | ||||
| <dl> | ||||
|     <dt> | ||||
|         <code>service</code> | ||||
|     </dt> | ||||
|     <dd> | ||||
|         The name of the service which hosts the resource. | ||||
|     </dd> | ||||
|     <dt> | ||||
|         <code>scope</code> | ||||
|     </dt> | ||||
|     <dd> | ||||
|         The resource in question, formatted as one of the space-delimited | ||||
|         entries from the <code>scope</code> parameters from the <code>WWW-Authenticate</code> header | ||||
|         shown above. This query parameter should be specified multiple times if | ||||
|         there is more than one <code>scope</code> entry from the <code>WWW-Authenticate</code> | ||||
|         header. The above example would be specified as: | ||||
|         <code>scope=repository:samalba/my-app:push</code>. | ||||
|     </dd> | ||||
|     <dt> | ||||
|         <code>account</code> | ||||
|     </dt> | ||||
|     <dd> | ||||
|         The name of the account which the client is acting as. Optional if it | ||||
|         can be inferred from client authentication. | ||||
|     </dd> | ||||
| </dl> | ||||
| 
 | ||||
| #### Description | ||||
| 
 | ||||
| Requests an authorization token for access to a specific resource hosted by a | ||||
| specific service provider. Requires the client to authenticate either using a | ||||
| TLS client certificate or using basic authentication (or any other kind of | ||||
| digest/challenge/response authentication scheme if the client doesn't support | ||||
| TLS client certs). If the key in the client certificate is linked to an account | ||||
| then the token is issued for that account key. If the key in the certificate is | ||||
| linked to multiple accounts then the client must specify the `account` query | ||||
| parameter. The returned token is in JWT (JSON Web Token) format, signed using | ||||
| the authorization server's private key. | ||||
| 
 | ||||
| #### Example | ||||
| 
 | ||||
| For this example, the client makes an HTTP request to the following endpoint | ||||
| over TLS using a client certificate with the server being configured to allow a | ||||
| non-verified issuer during the handshake (i.e., a self-signed client cert is | ||||
| okay). | ||||
| 
 | ||||
| ``` | ||||
| GET /v2/token/?service=registry.docker.com&scope=repository:samalba/my-app:push&account=jlhawn HTTP/1.1 | ||||
| Host: auth.docker.com | ||||
| ``` | ||||
| 
 | ||||
| The server first inspects the client certificate to extract the subject key and | ||||
| lookup which account it is associated with. The client is now authenticated | ||||
| using that account. | ||||
| 
 | ||||
| The server next searches its access control list for the account's access to | ||||
| the repository `samalba/my-app` hosted by the service `registry.docker.com`. | ||||
| 
 | ||||
| The server will now construct a JSON Web Token to sign and return. A JSON Web | ||||
| Token has 3 main parts: | ||||
| 
 | ||||
| 1.  Headers | ||||
| 
 | ||||
|     The header of a JSON Web Token is a standard JOSE header. The "typ" field | ||||
|     will be "JWT" and it will also contain the "alg" which identifies the | ||||
|     signing algorithm used to produce the signature. It will also usually have | ||||
|     a "kid" field, the ID of the key which was used to sign the token. | ||||
| 
 | ||||
|     Here is an example JOSE Header for a JSON Web Token (formatted with | ||||
|     whitespace for readability): | ||||
| 
 | ||||
|     ``` | ||||
|     { | ||||
|         "typ": "JWT", | ||||
|         "alg": "ES256", | ||||
|         "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6" | ||||
|     } | ||||
|     ``` | ||||
| 
 | ||||
|     It specifies that this object is going to be a JSON Web token signed using | ||||
|     the key with the given ID using the Elliptic Curve signature algorithm | ||||
|     using a SHA256 hash. | ||||
| 
 | ||||
| 2.  Claim Set | ||||
| 
 | ||||
|     The Claim Set is a JSON struct containing these standard registered claim | ||||
|     name fields: | ||||
| 
 | ||||
|     <dl> | ||||
|         <dt> | ||||
|             <code>iss</code> (Issuer) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             The issuer of the token, typically the fqdn of the authorization | ||||
|             server. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>sub</code> (Subject) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             The subject of the token; the id of the client which requested it. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>aud</code> (Audience) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             The intended audience of the token; the id of the service which | ||||
|             will verify the token to authorize the client/subject. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>exp</code> (Expiration) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             The token should only be considered valid up to this specified date | ||||
|             and time. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>nbf</code> (Not Before) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             The token should not be considered valid before this specified date | ||||
|             and time. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>iat</code> (Issued At) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             Specifies the date and time which the Authorization server | ||||
|             generated this token. | ||||
|         </dd> | ||||
|         <dt> | ||||
|             <code>jti</code> (JWT ID) | ||||
|         </dt> | ||||
|         <dd> | ||||
|             A unique identifier for this token. Can be used by the intended | ||||
|             audience to prevent replays of the token. | ||||
|         </dd> | ||||
|     </dl> | ||||
| 
 | ||||
|     The Claim Set will also contain a private claim name unique to this | ||||
|     authorization server specification: | ||||
| 
 | ||||
|     <dl> | ||||
|         <dt> | ||||
|             <code>access</code> | ||||
|         </dt> | ||||
|         <dd> | ||||
|             An array of access entry objects with the following fields: | ||||
| 
 | ||||
|             <dl> | ||||
|                 <dt> | ||||
|                     <code>type</code> | ||||
|                 </dt> | ||||
|                 <dd> | ||||
|                     The type of resource hosted by the service. | ||||
|                 </dd> | ||||
|                 <dt> | ||||
|                     <code>name</code> | ||||
|                 </dt> | ||||
|                 <dd> | ||||
|                     The name of the recource of the given type hosted by the | ||||
|                     service. | ||||
|                 </dd> | ||||
|                 <dt> | ||||
|                     <code>actions</code> | ||||
|                 </dt> | ||||
|                 <dd> | ||||
|                     An array of strings which give the actions authorized on | ||||
|                     this resource. | ||||
|                 </dd> | ||||
|             </dl> | ||||
|         </dd> | ||||
|     </dl> | ||||
| 
 | ||||
|     Here is an example of such a JWT Claim Set (formatted with whitespace for | ||||
|     readability): | ||||
| 
 | ||||
|     ``` | ||||
|     { | ||||
|         "iss": "auth.docker.com", | ||||
|         "sub": "jlhawn", | ||||
|         "aud": "registry.docker.com", | ||||
|         "exp": 1415387315, | ||||
|         "nbf": 1415387015, | ||||
|         "iat": 1415387015, | ||||
|         "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws", | ||||
|         "access": [ | ||||
|             { | ||||
|                 "type": "repository", | ||||
|                 "name": "samalba/my-app", | ||||
|                 "actions": [ | ||||
|                     "push" | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
|     ``` | ||||
| 
 | ||||
| 3.  Signature | ||||
| 
 | ||||
|     The authorization server will produce a JOSE header and Claim Set with no | ||||
|     extraneous whitespace, i.e., the JOSE Header from above would be | ||||
| 
 | ||||
|     ``` | ||||
|     {"typ":"JWT","alg":"ES256","kid":"PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"} | ||||
|     ``` | ||||
| 
 | ||||
|     and the Claim Set from above would be | ||||
| 
 | ||||
|     ``` | ||||
|     {"iss":"auth.docker.com","sub":"jlhawn","aud":"registry.docker.com","exp":1415387315,"nbf":1415387015,"iat":1415387015,"jti":"tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws","access":[{"type":"repository","name":"samalba/my-app","actions":["push"]}]} | ||||
|     ``` | ||||
| 
 | ||||
|     The utf-8 representation of this JOSE header and Claim Set are then | ||||
|     url-safe base64 encoded (sans trailing '=' buffer), producing: | ||||
| 
 | ||||
|     ``` | ||||
|     eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0 | ||||
|     ``` | ||||
| 
 | ||||
|     for the JOSE Header and | ||||
| 
 | ||||
|     ``` | ||||
|     eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0 | ||||
|     ``` | ||||
| 
 | ||||
|     for the Claim Set. These two are concatenated using a '.' character, | ||||
|     yielding the string: | ||||
| 
 | ||||
|     ``` | ||||
|     eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0 | ||||
|     ``` | ||||
| 
 | ||||
|     This is then used as the payload to a the `ES256` signature algorithm | ||||
|     specified in the JOSE header and specified fully in [Section 3.4 of the JSON Web Algorithms (JWA) | ||||
|     draft specification](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.4) | ||||
| 
 | ||||
|     This example signature will use the following ECDSA key for the server: | ||||
| 
 | ||||
|     ``` | ||||
|     { | ||||
|         "kty": "EC", | ||||
|         "crv": "P-256", | ||||
|         "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6", | ||||
|         "d": "R7OnbfMaD5J2jl7GeE8ESo7CnHSBm_1N2k9IXYFrKJA", | ||||
|         "x": "m7zUpx3b-zmVE5cymSs64POG9QcyEpJaYCD82-549_Q", | ||||
|         "y": "dU3biz8sZ_8GPB-odm8Wxz3lNDr1xcAQQPQaOcr1fmc" | ||||
|     } | ||||
|     ``` | ||||
| 
 | ||||
|     A resulting signature of the above payload using this key is: | ||||
| 
 | ||||
|     ``` | ||||
|     QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w | ||||
|     ``` | ||||
| 
 | ||||
|     Concatenating all of these together with a `.` character gives the | ||||
|     resulting JWT: | ||||
| 
 | ||||
|     ``` | ||||
|     eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w | ||||
|     ``` | ||||
| 
 | ||||
| This can now be placed in an HTTP response and returned to the client to use to | ||||
| authenticate to the audience service: | ||||
| 
 | ||||
| 
 | ||||
| ``` | ||||
| HTTP/1.1 200 OK | ||||
| Content-Type: application/json | ||||
| 
 | ||||
| {"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w"} | ||||
| ``` | ||||
| 
 | ||||
| ## Using the signed token | ||||
| 
 | ||||
| Once the client has a token, it will try the registry request again with the | ||||
| token placed in the HTTP `Authorization` header like so: | ||||
| 
 | ||||
| ``` | ||||
| Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWjpVQjVaOktJQVA6SU5QTDo1RU42Ok40SjQ6Nk1XTzpEUktFOkJWUUs6M0ZKTDpQT1RMIn0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJCQ0NZOk9VNlo6UUVKNTpXTjJDOjJBVkM6WTdZRDpBM0xZOjQ1VVc6NE9HRDpLQUxMOkNOSjU6NUlVTCIsImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5jb20iLCJleHAiOjE0MTUzODczMTUsIm5iZiI6MTQxNTM4NzAxNSwiaWF0IjoxNDE1Mzg3MDE1LCJqdGkiOiJ0WUpDTzFjNmNueXk3a0FuMGM3cktQZ2JWMUgxYkZ3cyIsInNjb3BlIjoiamxoYXduOnJlcG9zaXRvcnk6c2FtYWxiYS9teS1hcHA6cHVzaCxwdWxsIGpsaGF3bjpuYW1lc3BhY2U6c2FtYWxiYTpwdWxsIn0.Y3zZSwaZPqy4y9oRBVRImZyv3m_S9XDHF1tWwN7mL52C_IiA73SJkWVNsvNqpJIn5h7A2F8biv_S2ppQ1lgkbw | ||||
| ``` | ||||
| 
 | ||||
| This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1) | ||||
| 
 | ||||
| ## Verifying the token | ||||
| 
 | ||||
| The registry must now verify the token presented by the user by inspecting the | ||||
| claim set within. The registry will: | ||||
| 
 | ||||
| - Ensure that the issuer (`iss` claim) is an authority it trusts. | ||||
| - Ensure that the registry identifies as the audience (`aud` claim). | ||||
| - Check that the current time is between the `nbf` and `exp` claim times. | ||||
| - If enforcing single-use tokens, check that the JWT ID (`jti` claim) value has | ||||
|   not been seen before. | ||||
|   - To enforce this, the registry may keep a record of `jti`s it has seen for | ||||
|     up to the `exp` time of the token to prevent token replays. | ||||
| - Check the `access` claim value and use the identified resources and the list | ||||
|   of actions authorized to determine whether the token grants the required | ||||
|   level of access for the operation the client is attempting to perform. | ||||
| - Verify that the signature of the token is valid. | ||||
| 
 | ||||
| At no point in this process should the registry need to <em>call back</em> to | ||||
| the authorization server. If anything, it would only need to update a list of | ||||
| trusted public keys for verifying token signatures or use a separate API | ||||
| (still to be spec'd) to add/update resource records on the authorization | ||||
| server. | ||||
		Loading…
	
		Reference in New Issue