From d7cf5bd6c533d037662a964dcf282c1e8ab4b672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Tom=C3=A9=20Gon=C3=A7alves?= Date: Sun, 23 Aug 2026 21:02:08 +0100 Subject: [PATCH 1/3] Update authentication documentation Corrects the authentication section to match the current API behavior: the token response shape (expires_in is omitted for non-expiring tokens, token_type included), redirect_uri parameter and exact-match semantics, multiple redirect URI support, OAuth error responses, PAT creation authentication (requires an existing access token, not email/password), PAT revocation semantics, the restricted availability of email/password basic authentication, and the two-factor authentication headers (OTP-Token, OTP-Method-Id). Adds the request challenge endpoint to the One-Time Password section and the transactions:commit:otp scope. --- _applications.md | 5 ++- _authentication.md | 110 ++++++++++++++++++++++++++++++++++++--------- _totp.md | 26 +++++++++++ 3 files changed, 118 insertions(+), 23 deletions(-) diff --git a/_applications.md b/_applications.md index 0d2ee6d..0aa8643 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) (`http`, `ftp`, etc.). - 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..0c75fb4 100644 --- a/_authentication.md +++ b/_authentication.md @@ -28,11 +28,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 +52,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 +85,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: @@ -160,7 +190,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 +230,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 +246,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 +282,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 +296,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/_totp.md b/_totp.md index b9c399f..41e86db 100644 --- a/_totp.md +++ b/_totp.md @@ -134,3 +134,29 @@ curl https://api.uphold.com/v0/me/authentication_methods/3f8f8264-2f5e-4b2b-8333 ### Response Returns an HTTP status code of 204 and no JSON body, in case of success. + +## Request Authentication Method Challenge + +```bash +curl https://api.uphold.com/v0/me/authentication_methods/be95ed5f-d048-4348-9572-411df23bedc9/request_challenge \ + -X POST \ + -u : +``` + +> The above command does not return a JSON response. + +Requests the delivery of a verification code through the given authentication method, e.g. an SMS message. + +This endpoint can be authenticated with an access token or with basic authentication (email and password), so that a verification code can be obtained before completing a two-factor authentication challenge. + +### Request + +`POST https://api.uphold.com/v0/me/authentication_methods/:id/request_challenge` + + + +### Response + +Returns an HTTP status code of 204 and no JSON body, in case of success. From 8d28189b0c3587cd841ba35a58a1fab88930ab5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Tom=C3=A9=20Gon=C3=A7alves?= Date: Sun, 23 Aug 2026 21:02:08 +0100 Subject: [PATCH 2/3] Update rate limit values Updates the global rate limit to the current 500 requests per 5-minute window, removes the POST /password/reset row (no such limiter exists), adds the authentication method challenge and email-destination transaction limiters, and documents the too_many_requests error code. --- _ratelimits.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/_ratelimits.md b/_ratelimits.md index e4d413d..d5d772b 100644 --- a/_ratelimits.md +++ b/_ratelimits.md @@ -15,15 +15,18 @@ 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 +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. +## 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. diff --git a/_ratelimits.md b/_ratelimits.md index d5d772b..b299880 100644 --- a/_ratelimits.md +++ b/_ratelimits.md @@ -22,6 +22,7 @@ POST /me/authentication_methods/:id/request_challenge | 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