diff --git a/_applications.md b/_applications.md index 0d2ee6d..dd8ed8b 100644 --- a/_applications.md +++ b/_applications.md @@ -12,8 +12,8 @@ A registered application will be assigned a unique _Client Id_ and _Client Secre ## Considerations - For security reasons, your application **must** be secured with a valid _SSL_ certificate issued by a known Certificate Authority. -- Likewise, the provided _Redirect URL_ when registering the application must be a valid static subresource. Notice that this property cannot be dynamically reconfigured during authorization requests for security reasons. -- The _Redirect URL_ can also be a valid URI with a non-http/https protocol which is useful for mobile and desktop applications, for example: `my-app://uphold/connect`. +- An application can register one or more _Redirect URLs_. During an authorization request, the optional `redirect_uri` parameter must be an **exact match** of one of the registered values — there is no partial or wildcard matching, and this property cannot be dynamically reconfigured during authorization requests for security reasons. When omitted, the first registered _Redirect URL_ is used. +- A _Redirect URL_ must use the `https` scheme and include a host. For mobile and desktop applications, a custom application-specific scheme is also supported, for example: `my-app://uphold/connect`, as long as it does not collide with a registered [IANA scheme](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml) (`ftp`, `mailto`, etc.). Native applications may also register an `http` loopback _Redirect URL_ (host `127.0.0.1`, `[::1]` or `localhost`), which requires the [Native Application Flow (PKCE)](#native-application-flow-pkce) — see [Loopback Redirect URLs](#loopback-redirect-urls). - Users can revoke access to your application at any time. Your application **must** be prepared for this and, if necessary, should request authorization from the user again. - Likewise, when users change their password, all authorization tokens are expired and the user enters a cool-down period where outbound transactions are not allowed, for security reasons. Your application **must** be prepared for this. - Your application may be suspended in an automated fashion in accordance with our [Terms of Service](https://uphold.com/en/legal/membership-agreement). @@ -33,6 +33,7 @@ cards:read | Can view all [cards](#card-object) and their cards:write | Can create and update any [card](#card-object). phones:read | Can view all [phone](#phone-object) numbers and their information. phones:write | Can add new [phone](#phone-object) numbers. +transactions:commit:otp | Requires a Two Factor Authentication challenge when committing a [transaction](#transaction-object). transactions:deposit | Can create a deposit [transaction](#transaction-object). transactions:read | Can view any [transaction](#transaction-object). transactions:transfer:application | Can create a [transaction](#transaction-object) between the user and the application. diff --git a/_authentication.md b/_authentication.md index 48acf63..dc5fb2f 100644 --- a/_authentication.md +++ b/_authentication.md @@ -4,6 +4,7 @@ Uphold is an [OAuth 2.0](https://oauth.net/2/)-compliant service. Partners looking to integrate with our API must [register an application](#registering-an-application). Applications that implement a user-facing web interface, to provide custom functionality for multiple Uphold users, should use the [Web Application Flow](#web-application-flow). +Native applications — desktop, command-line or mobile — that receive the authorization redirect on a local loopback address should use the [Native Application Flow (PKCE)](#native-application-flow-pkce). Applications that implement a backend interface for a corporate partner (and therefore represent an Uphold user themselves) should use the [Client Credentials Flow](#client-credentials-flow). ## Web Application Flow @@ -28,11 +29,12 @@ https://wallet.uphold.com/authorize/?state=&scope=accou Supported query parameters: -Parameter | Required | Description ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- -intention | no | By default, unauthenticated users will be redirected to the `login` page. This behavior can be changed by sending `signup` as the `intention` value. -scope | yes | Permissions to request from the user. Multiple scopes [should be](https://tools.ietf.org/html/rfc6749#section-3.3) separated by spaces. -state | yes | An unguessable, cryptographically secure random string used to protect against cross-site request forgery attacks. +Parameter | Required | Description +------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- +intention | no | By default, unauthenticated users will be redirected to the `login` page. This behavior can be changed by sending `signup` as the `intention` value. +redirect_uri | no | Must be an **exact match** of one of the application's registered _Redirect URLs_. When omitted, the first registered _Redirect URL_ is used. +scope | yes | Permissions to request from the user. Multiple scopes [should be](https://tools.ietf.org/html/rfc6749#section-3.3) separated by spaces. +state | yes | An unguessable, cryptographically secure random string used to protect against cross-site request forgery attacks. ### Step 2 - Requesting a Token @@ -51,13 +53,17 @@ curl https://api.uphold.com/oauth2/token \ ```json { "access_token": "41ee8b1fa14042e031fe304bb4793b54e6576d19b306dc205136172b80d59d20", - "expires_in": null + "token_type": "bearer" } ``` If the user accepts your request, Uphold will redirect the user back to your site with a temporary `code` and the previously provided `state`, _as is_. -This temporary `code` is valid for a duration of **5 minutes** and **can only be used once**. +This temporary `code` is valid for a duration of **5 minutes** and **can only be used once** — repeated or concurrent attempts to redeem the same code fail with an `invalid_grant` error, in which case the user must go through the authorization flow again. + +By default, access tokens issued through this flow do not expire. +An `expires_in` field, indicating the token's lifetime in seconds, is only present when the application is configured with an access token lifetime. +Likewise, a `refresh_token` is only issued to applications with the refresh token grant enabled, and a `scope` field is present when the token is scoped. Your application is responsible for ensuring that the `state` matches the value previously provided, thus preventing a malicious third-party from forging this request. @@ -80,8 +86,33 @@ grant_type | yes | Must be set to *'authorization_code'*. +### Error Responses + +> Example of an error response when redeeming an authorization code twice: + +```json +{ + "error": "invalid_grant", + "error_description": "Authorization code is invalid" +} +``` + +Failed requests to the token endpoint return an [RFC 6749](https://tools.ietf.org/html/rfc6749#section-5.2)-style error response, where `error` is a machine-readable code and `error_description` a human-readable explanation. + +The most common error codes are: + +Error | HTTP status | Typical cause +------------------- | ---------------------------------------------- | -------------------------------------------------------------- +invalid_request | 400 | A required parameter is missing or malformed. +invalid_client | 400 (401 when using HTTP Basic Authentication) | Unknown *clientId* or wrong *clientSecret*. +invalid_grant | 400 | The authorization code is invalid, expired or already used. +invalid_scope | 400 | The requested scope is not available to the application. +unauthorized_client | 400 | The grant type is not enabled for the application. + ### Step 3 - Using the Access Token > Request using the 'Authorization' header: @@ -101,6 +132,92 @@ Once you have obtained an access token you may call any protected API method on This prevents attackers from stealing tokens from the user's browser history, logs, referrer headers and other insecure locations when credentials are sent via query URLs. +## Native Application Flow (PKCE) + +Ideal for desktop, command-line and mobile applications — public clients that cannot keep a secret and receive the authorization redirect on a local loopback address. + +This flow extends the [Web Application Flow](#web-application-flow) with a Proof Key for Code Exchange ([PKCE, RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)), which binds the authorization code to a per-attempt secret so that an intercepted code cannot be redeemed by anyone else. + +Any authorization redirecting to a loopback _Redirect URL_ (see [Loopback Redirect URLs](#loopback-redirect-urls)) **must** use this flow; the same application's `https` authorizations are unaffected. Applications may additionally be enrolled to require PKCE on every authorization by [contacting us](#support). + +### Step 1 - Generate a Proof Key + +> Example of generating a verifier and challenge: + +```bash +code_verifier=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_') +code_challenge=$(printf '%s' "$code_verifier" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_') +``` + +For every authorization attempt, generate a fresh cryptographically random `code_verifier` — 43 to 128 characters from the set `A-Z a-z 0-9 - . _ ~` — and derive the `code_challenge` as the base64url-encoded SHA-256 digest of the verifier. + +Only the challenge is sent when starting the flow; the verifier never leaves your application until the token exchange. + +### Step 2 - Authorization + +> Example of an authorization start URL: + +``` +https://api.uphold.com/oauth2/authorize/start?client_id=&code_challenge=&code_challenge_method=S256&state=&scope=accounts:read%20cards:read +``` + +Direct the user's browser to the following endpoint, which stores the challenge and redirects the user to the Uphold authorization page: + +`GET https://api.uphold.com/oauth2/authorize/start` + +Or for Sandbox applications: + +`GET https://api-sandbox.uphold.com/oauth2/authorize/start` + +Supported query parameters: + +Parameter | Required | Description +--------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- +client_id | yes | The application's *clientId*. +code_challenge | yes | The base64url-encoded SHA-256 digest of the `code_verifier` (43 characters). +code_challenge_method | yes | Must be set to *'S256'*. +redirect_uri | no | Must match one of the application's registered _Redirect URLs_ (see [Loopback Redirect URLs](#loopback-redirect-urls)). When omitted, the first registered _Redirect URL_ is used. +scope | yes | Permissions to request from the user. Multiple scopes [should be](https://tools.ietf.org/html/rfc6749#section-3.3) separated by spaces. +state | yes | An unguessable, cryptographically secure random string (up to 500 characters), unique per authorization attempt. + +The challenge is bound to the (`client_id`, `state`) pair and is valid for **30 minutes**. +Restarting an abandoned authorization with the same `state` and the same challenge is allowed, but reusing a `state` with a *different* challenge fails with an `invalid_request` error until the previous challenge expires — generate a fresh `state` for every attempt. + +As in the Web Application Flow, once the user accepts your request they are redirected back to your application with a temporary single-use `code` and the previously provided `state`. + +### Step 3 - Requesting a Token + +> Exchanging the `code` and `code_verifier` for a `token`: + +```bash +curl https://api.uphold.com/oauth2/token \ + -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -u : \ + -d 'code=&code_verifier=&grant_type=authorization_code' +``` + +Exchange the code exactly as in the [Web Application Flow](#step-2-requesting-a-token), additionally sending the verifier generated in step 1: + +Parameter | Required | Description +------------- | -------- | ------------------------------------------------------------- +code_verifier | yes | The `code_verifier` this authorization attempt started with. + +The API hashes the verifier and compares it against the challenge bound to the code. +A missing or mismatched verifier fails with an `invalid_grant` error **and consumes the code** — the user must go through the authorization flow again. + +### Loopback Redirect URLs + +Native applications typically listen for the authorization redirect on a random port of the loopback interface, which cannot be known at registration time. + +Register a _Redirect URL_ with the `http` scheme and the host `127.0.0.1`, `[::1]` or `localhost` — for example, `http://127.0.0.1:8080/callback`. +During an authorization request, the **port is ignored** when matching a loopback `redirect_uri` against the registered value, following [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3): a registration of `http://127.0.0.1:8080/callback` matches `http://127.0.0.1:53127/callback`. +The scheme, host, path and query must still match exactly, and the port must be numeric. + + + ## Client Credentials Flow Ideal for backend integrations that do not require access to other Uphold user accounts. @@ -160,7 +277,8 @@ Once you have obtained a client credentials token you may call any protected API Ideal for scripts, automated tools and command-line programs which remain under your control. For **personal usage only** you may choose to use a PAT. -This token establishes who you are, provides full access to your user account and bypasses Two Factor Authentication, if enabled. +This token establishes who you are, provides full access to your user account and does not expire until revoked. +Authenticating with a PAT does not require a Two Factor Authentication challenge; however, individual operations that require a one-time password — such as creating another PAT or committing certain transactions — will still request one (see [Two-Factor Authentication](#two-factor-authentication)). For this reason it should be treated just like your email/password combination, i.e. remain secret and never shared with third parties. PATs can be issued and revoked individually. @@ -199,9 +317,9 @@ To list Personal Access Tokens you may use the following endpoint: ```bash curl https://api.uphold.com/v0/me/tokens \ -X POST \ + -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "OTP-Token: " \ - -u : \ -d '{ "description": "My command line script" }' ``` @@ -215,18 +333,23 @@ curl https://api.uphold.com/v0/me/tokens \ } ``` -To create a Personal Access Token you may use the following endpoint: +To create a Personal Access Token you may use the following endpoint, authenticating with an existing access token (obtained through an OAuth flow or another PAT): `POST https://api.uphold.com/v0/me/tokens` Supported parameters: Parameter | Required | Description ------------ | -------- | ----------------------------------------- -description | yes | A human-readable description of this PAT. +----------- | -------- | ------------------------------------------------------------ +description | yes | A human-readable description of this PAT (1 to 255 characters). + + ### Revoking a PAT @@ -246,8 +369,10 @@ To revoke a Personal Access Token you may use the following endpoint: Supported parameters: Parameter | Required | Description ---------- | -------- | --------------------------- -token | yes | The PAT you wish to revoke. +--------- | -------- | ----------------------------------------------------------------------------------- +token | yes | The `accessToken` value of the PAT you wish to revoke (not its `id`). + +Returns an HTTP status code of `204` and no body in case of success, or a [404 HTTP error](#errors) if the token does not exist. ### Using a PAT @@ -258,21 +383,51 @@ curl https://api.uphold.com/v0/me \ -H "Authorization: Bearer " ``` +> Alternatively, a PAT can be sent via HTTP Basic Authentication, using the token as the username and `x-oauth-basic` as the password: + +```bash +curl https://api.uphold.com/v0/me \ + -u :x-oauth-basic +``` + A PAT may be used for authenticating a request via the OAuth scheme. The `` should be set as the `accessToken` received during creation. ## Basic Authentication -> Simple request using email and password: +> Example of listing your authentication methods using email and password: ```bash -curl https://api.uphold.com/v0/me \ - -H 'OTP-Token: ' \ +curl https://api.uphold.com/v0/me/authentication_methods \ -u : ``` -You can use Basic Authentication by providing your email and password combination. +Authenticating with your email and password combination is restricted to a small set of endpoints used to manage and bootstrap Two Factor Authentication: + +- `GET /v0/me/authentication_methods` +- `POST /v0/me/authentication_methods/:id/request_challenge` +- `GET /v0/me/phones` + +All other endpoints require an OAuth access token or a [Personal Access Token](#personal-access-tokens-pat). + +## Two-Factor Authentication + +> Example of a response requiring a one-time password: + +``` +HTTP/1.1 401 Unauthorized + +OTP-Token: required +``` + +When an operation requires a one-time password (OTP, also known as Two-Factor Authentication), the API responds with a [401 HTTP error](#errors) and the response header `OTP-Token: required`. + +In that case, repeat the request with the following headers: + +Header | Required | Description +------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- +OTP-Token | yes | The one-time password (verification code). +OTP-Method-Id | no | The `id` of the [authentication method](#one-time-password) to verify against. Defaults to the user's default authentication method. -If OTP (One-Time Password, also known as Two-Factor Authentication) is required, then you will get a [401 HTTP error](#errors), along with the HTTP header `OTP-Token: Required`. -In which case, execute the command above again, this time passing your OTP verification code header, like so: `OTP-Token: `. +For SMS-based authentication methods, a verification code must first be requested through the [request challenge endpoint](#one-time-password). diff --git a/_ratelimits.md b/_ratelimits.md index e4d413d..b299880 100644 --- a/_ratelimits.md +++ b/_ratelimits.md @@ -15,15 +15,19 @@ but multiple IPs can only make 5 requests per 60 minute time period per user acc The following table indicates the current rate limits: -Endpoint | Requests (per IP) / window | Requests (per user) / window ------------------------------------------ | -------------------------: | ---------------------------: -*Global* | 250 / 1-min window | N/A -POST /me/confirm | 10 / 10-min window | N/A -POST /me/reports | 10 / 10-min window | 10 / 10-min window -POST /oauth2/token | 50 / 5-min window | 50 / 5-min window -POST /password/change | 5 / 60-min window | 5 / 60-min window -POST /password/forgot | 5 / 60-min window | 5 / 60-min window -POST /password/reset | 5 / 60-min window | N/A +Endpoint | Requests (per IP) / window | Requests (per user) / window +------------------------------------------------------- | -------------------------: | ---------------------------: +*Global* | 500 / 5-min window | N/A +POST /me/authentication_methods/:id/request_challenge | N/A | 1 / 45-sec window +POST /me/cards/:card/transactions *(email destination)* | N/A | 5 / 60-min window +POST /me/confirm | 10 / 10-min window | N/A +POST /me/reports | 10 / 10-min window | 10 / 10-min window +GET /oauth2/authorize/start | 30 / 5-min window | N/A +POST /oauth2/token | 50 / 5-min window | 50 / 5-min window +POST /password/change | 5 / 60-min window | 5 / 60-min window +POST /password/forgot | 5 / 60-min window | 5 / 60-min window + +For unauthenticated endpoints such as `POST /oauth2/token` and `POST /password/forgot`, the per-user dimension is keyed on the username or email address provided in the request.