diff --git a/.ruby-version b/.ruby-version index 49cdd668e1c..6a81b4c8379 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.6 +2.7.8 diff --git a/Gemfile b/Gemfile index 95029e39a5c..99043e67b4f 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,5 @@ gem 'middleman-autoprefixer', '~> 2.7' gem 'middleman-sprockets', '~> 4.1' gem 'rouge', '~> 3.2' gem 'redcarpet', '~> 3.4.0' -gem 'nokogiri', '~> 1.10.8' +gem 'nokogiri', '>= 1.11.0' gem 'sass' diff --git a/Gemfile.lock b/Gemfile.lock index 8d2f9eb0d8c..eb99a93f62a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,10 +76,11 @@ GEM middleman-syntax (3.2.0) middleman-core (>= 3.2) rouge (~> 3.2) - mini_portile2 (2.4.0) + mini_portile2 (2.8.9) minitest (5.14.1) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) + nokogiri (1.15.7) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) padrino-helpers (0.13.3.4) i18n (~> 0.6, >= 0.6.7) padrino-support (= 0.13.3.4) @@ -88,6 +89,7 @@ GEM activesupport (>= 3.1) parallel (1.19.1) public_suffix (4.0.5) + racc (1.8.1) rack (2.2.2) rb-fsevent (0.10.4) rb-inotify (0.10.1) @@ -122,7 +124,7 @@ DEPENDENCIES middleman-autoprefixer (~> 2.7) middleman-sprockets (~> 4.1) middleman-syntax (~> 3.2) - nokogiri (~> 1.10.8) + nokogiri (>= 1.11.0) redcarpet (~> 3.4.0) rouge (~> 3.2) sass diff --git a/source/index.html.md b/source/index.html.md index 7cc70b95a1a..2c372b79ef4 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -18,6 +18,15 @@ While the API is a self-serve tool, we have compiled some customer FAQs in [Expe # Authentication +The `credentials` object in every `requestJobDescription` accepts either method: + +Method | Fields | When to use +------ | ------ | ----------- +Credential based | `partnerUserID` + `partnerUserSecret` | Server-to-server integrations with a single Expensify account +OAuth2 access token | `authToken` | Multi-user integrations acting on behalf of individual Expensify users + +## Credential based authentication + To use the API, you will need to generate API credentials. 1. Create an Expensify account at @@ -28,6 +37,89 @@ To use the API, you will need to generate API credentials. Make sure to store the partnerUserID and partnerUserSecret pair you're given in a secure location, as you won't be shown them again. +## OAuth2 partner authentication + +To act on behalf of individual Expensify users, use the OAuth2 authorization code flow to obtain a short-lived access token. Pass that token as `credentials.authToken` in your requests. + +**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com). + + + +**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating. + +### Step 1 — Redirect the user to authorize + +``` +https://www.expensify.com/oauth/authorize + ?response_type=code + &scope=integrations:api + &client_id=YOUR_CLIENT_ID + &redirect_uri=YOUR_REDIRECT_URI + &state=RANDOM_STATE_VALUE +``` + +Before redirecting the user, generate a new cryptographically random `state` value on your server — for example, a UUID or a hex string from a secure random source — and store it in the user's session. Use a different value for every authorization request; never reuse a previous one. When the user returns to your callback URL, confirm that the `state` parameter matches what you stored, and reject the request if it does not or if it is missing. + +### Step 2 — Exchange the authorization code for tokens + +After the user authorizes, Expensify redirects to `YOUR_REDIRECT_URI?code=AUTH_CODE&state=...`. Exchange the code for tokens: + +```shell +curl -X POST 'https://www.expensify.com/oauth/token' \ + -d 'grant_type=authorization_code' \ + -d 'code=AUTH_CODE' \ + -d 'redirect_uri=YOUR_REDIRECT_URI' \ + -d 'client_id=YOUR_CLIENT_ID' \ + -d 'client_secret=YOUR_CLIENT_SECRET' +``` + +Response: + +```json +{ + "access_token": "ABCDEF123...", + "refresh_token": "GHIJKL456...", + "expires_in": 7200 +} +``` + +- `access_token` expires in approximately 2 hours. Store it in memory only. +- `refresh_token` does not expire. Store it persistently (for example, in a database). +- Authorization codes expire in 2 minutes and can only be used once. + +### Step 3 — Use the access token in IS requests + +Pass `access_token` as `credentials.authToken` in any `requestJobDescription`: + +```shell +curl -X POST 'https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations' \ + -d 'requestJobDescription={ + "type":"get", + "credentials":{ + "authToken":"ABCDEF123..." + }, + "inputSettings":{ + "type":"policyList" + } + }' +``` + +### Step 4 — Refresh the access token + +When the `access_token` expires, exchange the `refresh_token` for a new pair: + +```shell +curl -X POST 'https://www.expensify.com/oauth/token' \ + -d 'grant_type=refresh_token' \ + -d 'refresh_token=GHIJKL456...' \ + -d 'client_id=YOUR_CLIENT_ID' \ + -d 'client_secret=YOUR_CLIENT_SECRET' +``` + +Always store the new `refresh_token` returned in the response — each refresh issues a new one and invalidates the old one. + # Request format > API call format @@ -62,7 +154,7 @@ For every request, the `requestJobDescription` JSON parameter will need to conta Parameter | Type | Description --------- | ------- | ----------- type | String | The type of job to execute -credentials | JSON object | An object containing two key/values used to authenticate you: `partnerUserID` and `partnerUserSecret`. +credentials | JSON object | An object with either `partnerUserID` + `partnerUserSecret` (partner credentials) or a single `authToken` (OAuth2 access token). See [Authentication](#authentication). `inputSettings` | JSON Object | Additional information about the job to execute # Rate limits