Skip to content

Commit cc9c191

Browse files
authored
Merge pull request #155 from XeroAPI/jwt_verification
JWT & State verification, Readme cleanup, token helpers
2 parents cd547e7 + d82ac6e commit cc9c191

File tree

11 files changed

+324
-120
lines changed

11 files changed

+324
-120
lines changed

Gemfile.lock

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
PATH
22
remote: .
33
specs:
4-
xero-ruby (2.9.1)
4+
xero-ruby (2.10.0)
55
faraday (~> 1.0, >= 1.0.1)
66
json (~> 2.1, >= 2.1.0)
7+
json-jwt (~> 1.5, >= 1.5.2)
78

89
GEM
910
remote: https://rubygems.org/
1011
specs:
12+
activesupport (6.0.3.4)
13+
concurrent-ruby (~> 1.0, >= 1.0.2)
14+
i18n (>= 0.7, < 2)
15+
minitest (~> 5.1)
16+
tzinfo (~> 1.1)
17+
zeitwerk (~> 2.2, >= 2.2.2)
18+
aes_key_wrap (1.1.0)
1119
ast (2.4.1)
20+
bindata (2.4.9)
1221
byebug (11.1.3)
1322
coderay (1.1.3)
23+
concurrent-ruby (1.1.8)
1424
diff-lcs (1.4.4)
1525
faraday (1.4.1)
1626
faraday-excon (~> 1.1)
@@ -21,9 +31,16 @@ GEM
2131
faraday-excon (1.1.0)
2232
faraday-net_http (1.0.1)
2333
faraday-net_http_persistent (1.1.0)
34+
i18n (1.8.7)
35+
concurrent-ruby (~> 1.0)
2436
jaro_winkler (1.5.4)
2537
json (2.5.1)
38+
json-jwt (1.13.0)
39+
activesupport (>= 4.2)
40+
aes_key_wrap
41+
bindata
2642
method_source (1.0.0)
43+
minitest (5.14.3)
2744
multipart-post (2.1.1)
2845
parallel (1.20.1)
2946
parser (2.7.2.0)
@@ -60,7 +77,11 @@ GEM
6077
unicode-display_width (>= 1.4.0, < 1.6)
6178
ruby-progressbar (1.10.1)
6279
ruby2_keywords (0.0.4)
80+
thread_safe (0.3.6)
81+
tzinfo (1.2.9)
82+
thread_safe (~> 0.1)
6383
unicode-display_width (1.5.0)
84+
zeitwerk (2.4.2)
6485

6586
PLATFORMS
6687
ruby

README.md

Lines changed: 132 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,39 @@ Xero Ruby SDK for OAuth 2.0 generated from [Xero API OpenAPI Spec](https://githu
55

66
# Documentation
77
Xero Ruby SDK supports Xero's OAuth2.0 authentication and the following Xero API sets.
8-
9-
## SDK Documentation
8+
### API Client Documentation
109
* [API client methods](https://xeroapi.github.io/xero-ruby/accounting/index.html)
11-
---
12-
## API Model Docs
13-
* [Accounting Models](/docs/accounting)
14-
* [Asset Api Docs](/docs/assets/)
15-
* [Project Api Docs](docs/projects/)
16-
* [Files Api Docs](docs/files/)
17-
* [Payroll Docs (AU)](docs/payroll_au/)
18-
* [Payroll Docs (NZ)](docs/payroll_nz/)
19-
* [Payroll Docs (UK)](docs/payroll_uk/)
20-
10+
> This describes to ~200+ accounting API endpoints and their expected params. There are also method reference docs for the Assets, Files, Projects, and Payroll endpoints sets, though we are still working on accurately generating usable parameter examples for all! (feedback welcome)
11+
### Model Docs
12+
* [Models](/docs/)
13+
> Directory of markdown files, describing the object models for the Accounting, Asset, Projects, Files, Payroll (AU, UK, NZ) Xero API sets.
2114
## Sample Apps
22-
We have two apps showing SDK usage.
23-
* https://github.com/XeroAPI/xero-ruby-oauth2-starter (**Sinatra** - session based / getting started)
24-
* https://github.com/XeroAPI/xero-ruby-oauth2-app (**Rails** - token management / full examples)
15+
We have two sample apps showing SDK usage:
16+
* https://github.com/XeroAPI/xero-ruby-oauth2-starter (**Sinatra** - bare minimum to hello world and simple session based storage)
17+
* https://github.com/XeroAPI/xero-ruby-oauth2-app (**Rails** - token management with robust usage examples)
2518

2619
![sample-app](https://i.imgur.com/OOEn55G.png)
2720

28-
---
21+
## Xero Pre-Requisites
22+
* Create a [free Xero user account](https://www.xero.com/us/signup/api/)
23+
* Login to your Xero developer [/myapps](https://developer.xero.com/myapps) dashboard & create an API application
24+
* Copy the credentials from your API app and store/access them using a secure ENV variable strategy
25+
* Resaearch and include the [neccesary scopes](https://developer.xero.com/documentation/oauth2/scopes) for your app's functionality as a space-seperated list, ex. "`SCOPES="openid profile email accounting.transactions accounting.settings"`"
26+
27+
28+
2929
## Installation
30-
To install this gem to your current gemset.
30+
To install this gem to your project:
3131
```
3232
gem install 'xero-ruby'
3333
```
34-
Or add to your gemfile and run `bundle install`.
34+
Or more commonly in Ruby on Rails usage add to your gemfile and run `bundle install`:
3535
```
3636
gem 'xero-ruby'
3737
```
3838

39-
## Getting Started
40-
* Create a [free Xero user account](https://www.xero.com/us/signup/api/)
41-
* Login to your Xero developer [/myapps](https://developer.xero.com/myapps) dashboard & create an API application and note your API app's credentials.
42-
43-
### Creating a client
44-
* Get the credential values from an API application at https://developer.xero.com/myapps/.
45-
* Include [neccesary scopes](https://developer.xero.com/documentation/oauth2/scopes) as a space-seperated list
46-
* example => "`openid profile email accounting.transactions accounting.settings`"
39+
---
40+
## Usage
4741
```
4842
require 'xero-ruby'
4943
```
@@ -58,56 +52,77 @@ creds = {
5852
xero_client ||= XeroRuby::ApiClient.new(credentials: creds)
5953
```
6054

61-
If you want additional logging or timeout, you can add/override any configuration option by passing the optional named parameter object `config`.
55+
For additional logging or timeout, add or override any [config](/lib/xero-ruby/configuration.rb) option by passing an optional named parameter `config: {..}`.
6256
```ruby
6357
config = { timeout: 30, debugging: true }
6458
@xero_client ||= XeroRuby::ApiClient.new(credentials: creds, config: config)
6559
```
6660

67-
## User Authorization & Callback
68-
All API requests require a valid access token to be set on the client.
61+
## OAuth2.0 Authorization & Callback
62+
All API requests require a valid access token to be set on the xero_client.
6963

70-
To generate a valid `token_set` send a user to the `authorization_url`:
64+
### Step 1
65+
Send the user to the `authorization_url` after you have configured your xero_client
7166
```ruby
7267
@authorization_url = xero_client.authorization_url
7368

7469
redirect_to @authorization_url
7570
```
7671

77-
Xero will then redirect back to the URI defined in your `redirect_uri` config.
78-
79-
*This must match **exactly** with the variable in your /myapps dashboard.*
80-
81-
In your callback, calling `get_token_set_from_callback` will exchange the temporary code Xero return, with a valid `token_set` that you can use to make API calls.
72+
### Step 2
73+
On successful authorization, Xero identity will redirect to the URI defined in your `redirect_uri` config which must match **exactly** with the variable in your /myapps dashboard.
74+
```
75+
=> /oauth/redirect_uri
76+
```
77+
### Step 3
78+
In your server defined callback route, exchange the temporary code for a valid `token_set` that will get set on your client.
8279
```ruby
83-
# => http://localhost:3000/oauth/callback
84-
8580
token_set = xero_client.get_token_set_from_callback(params)
81+
```
82+
At this point you should save the token_set as JSON in a datastore in relation to the authenticating user or entity.
8683

87-
# save token_set JSON in a datastore in relation to the user authentication
88-
89-
puts params['state']
90-
=> "this-can-be-a-custom-state-parameter"
84+
The sample [Rails app](https://github.com/XeroAPI/xero-ruby-oauth2-app/blob/master/app/controllers/application_controller.rb#L11) shows a solid pattern you can tweak to fit your needs:
85+
```ruby
86+
# /oauth/redirect_uri -> 'application#callback'
87+
def callback
88+
@token_set = @xero_client.get_token_set_from_callback(params)
89+
90+
current_user.token_set = @token_set
91+
current_user.token_set['connections'] = @xero_client.connections
92+
current_user.active_tenant_id = latest_connection(current_user.token_set['connections'])
93+
current_user.save!
94+
flash.notice = "Successfully authenticated with Xero!"
95+
end
9196
```
97+
---
98+
### What is a Token Set?
99+
A `token_set` is what we call the XeroAPI response that contains data about your API connection:
100+
```json
101+
{
102+
"id_token": "xxx.yyy.zz", (if you requested `openid profile email` scope)
103+
"access_token": "xxx.yyy.zzz",
104+
"expires_in": 1800,
105+
"token_type": "Bearer",
106+
"refresh_token": "xxxxxx", (if you requested `offline_access` scope)
107+
"scope": "email profile openid accounting.transactions offline_access"
108+
}
109+
```
110+
111+
Note that an `access_token` is valid for 30 minutes but a `refresh_token` can be used once in up to a 60 day window. If a refresh_token is used to refresh access you must replace the entire token_set.
92112

93-
## Making API calls once you have a token_set
94-
Once you already have a token_set stored from this initual user interaction, you can setup a new client by passing the whole token_set to `refresh_token_set` or `set_token_set`.
113+
Both the `id_token` & `access_token` are JWT's, and can be decoded for to see additional metadata described in the Token Helpers section:
114+
## Making API calls with a valid token_set
115+
After the initial user interaction you can simply setup a xero_client by passing the whole token_set to the client.
95116
```ruby
96117
xero_client.set_token_set(user.token_set)
97118

98119
xero_client.refresh_token_set(user.token_set)
99-
# this will set the access_token on the client, and return a refreshed `token_set` you need to save.
100120
```
101-
A `token_set` contains data about your API connection most importantly :
102-
* `access_token`
103-
* `refresh_token`
104-
* `expiry`
105-
106-
**An `access_token` is valid 30 minutes and a `refresh_token` is valid for 60 days**
107-
108-
Example Token set:
109-
> You can decode the `id_token` & `access_token` for additional metadata by using a [decoding library](https://github.com/jwt/ruby-jwt):
110-
```json
121+
This sets the access_token on the client, and returns a refreshed `token_set` you should save in your database for the next time you need to connect to Xero's API.
122+
## Token Helpers
123+
```ruby
124+
xero_client.token_set
125+
=>
111126
{
112127
"id_token": "xxx.yyy.zz",
113128
"access_token": "xxx.yyy.zzz",
@@ -116,17 +131,63 @@ Example Token set:
116131
"refresh_token": "xxxxxx",
117132
"scope": "email profile openid accounting.transactions offline_access"
118133
}
119-
```
120134

121-
## Token & SDK Helpers
122-
Refresh/connection helpers
135+
xero_client.access_token
136+
=> "xxx.yyy.zzz"
137+
138+
xero_client.decoded_access_token
139+
=> {
140+
"exp": 1619715843,
141+
"xero_userid": "xero-user-uuid",
142+
"scope": [
143+
"email",
144+
"profile",
145+
"openid",
146+
"accounting.transactions",
147+
"offline_access"
148+
]
149+
}
150+
151+
152+
xero_client.id_token
153+
=> "aaa.bbb.ccc"
154+
155+
xero_client.decoded_id_token
156+
=> {
157+
"iss": "https://identity.xero.com",
158+
"email": "luca.pacioli@accounting-services.com",
159+
"given_name": "Luca",
160+
"family_name": "Pacioli"
161+
}
162+
163+
xero_client.set_token_set(token_set)
164+
=> true
165+
166+
xero_client.get_token_set_from_callback(callback_url_params)
167+
=> new_xero_token_set
168+
169+
xero_client.refresh_token_set(token_set)
170+
=> new_xero_token_set
171+
172+
# These are automatically populated with `set_token_set`
173+
# But if you need to set just an access or id token on the client
174+
xero_client.set_access_token(access_token)
175+
xero_client.set_id_token(id_token)
176+
177+
# Automatically run on initial OAuth flow - can be called its own if desired
178+
# Read about why we have included this in the default library: https://auth0.com/docs/tokens/access-tokens/validate-access-tokens
179+
xero_client.validate_tokens(token_set)
180+
xero_client.decode_jwt(tkn)
181+
```
182+
# Connection Helpers
123183
```ruby
124-
@token_set = xero_client.refresh_token_set(user.token_set)
184+
xero_client.authorization_url
185+
=> # https://login.xero.com/identity/connect/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&scope=<scopes>&state=<my-state>
125186

126-
# Xero's tokens can potentially facilitate (n) org connections in a single token.
127-
# It is important to store the `tenantId` of the Organisation your user wants to read/write data.
187+
# To completely Revoke a user's access token and all their connections
188+
xero_client.revoke_token(token_set)
128189

129-
# The `updatedDateUtc` will show you the most recently authorized Tenant (AKA Organisation)
190+
# In case there are > 1 tenants connected the `updatedDateUtc` will show you the most recently authorized tenant (aka organisation) - it is important to store the `tenantId` of the Org your user specified in their API authorization
130191
connections = xero_client.connections
131192
[{
132193
"id" => "xxx-yyy-zzz",
@@ -137,38 +198,19 @@ connections = xero_client.connections
137198
"updatedDateUtc" => "2020-04-15T22:37:10.4943410"
138199
}]
139200

140-
# To completely Revoke a user's access token and all their connections
141-
# pass in the users token set to the #revoke_token api_client method
142-
143-
xero_client.revoke_token(user.token_set)
144-
145-
# disconnect an org from a user's connections. Pass the connection ['id'] not ['tenantId'].
146-
# Useful if you want to enforce only a single org connection per token.
201+
# To disconnect a single org from a user's active connections pass the connection ['id'] (not ['tenantId'])
202+
# If you want to enforce only a single org connection per token do this prior to sending user through Xero authorize flow a 2nd time.
147203
remaining_connections = xero_client.disconnect(connections[0]['id'])
148204

149-
# set a refreshed token_set
150-
token_set = xero_client.set_token_set(user.token_set)
205+
xero_client.token_expired?
206+
=> true || false
151207

152-
# access token_set once it is set on the client
153-
token_set = xero_client.token_set
208+
# This will check against the following logic
209+
token_expiry = Time.at(decoded_access_token['exp'])
210+
token_expiry < Time.now
154211
```
155212

156-
Example token expiry helper
157-
```ruby
158-
require 'jwt'
159-
160-
def token_expired?
161-
token_expiry = Time.at(decoded_access_token['exp'])
162-
token_expiry < Time.now
163-
end
164-
165-
def decoded_access_token
166-
JWT.decode(token_set['access_token'], nil, false)[0]
167-
end
168-
```
169-
170-
## API Usage
171-
213+
# API Usage
172214
### Accounting API
173215
> https://xeroapi.github.io/xero-ruby/accounting/index.html
174216
```ruby
@@ -177,7 +219,7 @@ require 'xero-ruby'
177219
xero_client.refresh_token_set(user.token_set)
178220

179221
tenant_id = user.active_tenant_id
180-
# example of how to store the `tenantId` of the specific tenant (aka organisation)
222+
# Example 'active tenant' logic storage of the tenant the user specified, xero_client.connections[0] is not a safe assumption in case they authorized multiple orgs.
181223

182224
# Get Accounts
183225
accounts = xero_client.accounting_api.get_accounts(tenant_id).accounts
@@ -331,19 +373,13 @@ opts = {
331373
}
332374
xero_client.accounting_api.get_bank_transfers(tenant_id, opts).bank_transfers
333375
```
334-
### NOTE
376+
335377
1) Not all `opts` parameter combinations are available for all endpoints, and there are likely some undiscovered edge cases. If you encounter a filter / sort / where clause that seems buggy open an issue and we will dig.
336378

337379
2) Some opts string values may need PascalCasing to match casing defined in our [core API docs](https://developer.xero.com/documentation/api/api-overview).
338380
* `opts = { order: 'UpdatedDateUtc DESC'}`
339381

340382
3) If you have use cases outside of these examples let us know.
341-
342-
## Sample App
343-
The best resource to understanding how to best leverage this SDK is the sample applications showing all the features of the gem.
344-
> https://github.com/XeroAPI/xero-ruby-oauth2-starter (Sinatra - simple getting started)
345-
> https://github.com/XeroAPI/xero-ruby-oauth2-app (Rails - full featured examples)
346-
347383
## Developing locally
348384
To develop this gem locally against your project you can use the following development pattern:
349385

docs/accounting/AccountingApi.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6623,7 +6623,9 @@ opts = {
66236623

66246624
page: 1, # Integer | e.g. page=1 - Up to 100 contacts will be returned in a single API call.
66256625

6626-
include_archived: true # Boolean | e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response
6626+
include_archived: true, # Boolean | e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response
6627+
6628+
summary_only: false # Boolean | Use summaryOnly=true in GET Contacts endpoint to retrieve a smaller version of the response object. This returns only lightweight fields, excluding computation-heavy fields from the response, making the API calls quick and efficient.
66276629
}
66286630

66296631
begin
@@ -6647,6 +6649,7 @@ Name | Type | Description | Notes
66476649
**i_ds** | [**Array&lt;String&gt;**](String.md)| Filter by a comma separated list of ContactIDs. Allows you to retrieve a specific set of contacts in a single call. | [optional]
66486650
**page** | **Integer**| e.g. page&#x3D;1 - Up to 100 contacts will be returned in a single API call. | [optional]
66496651
**include_archived** | **Boolean**| e.g. includeArchived&#x3D;true - Contacts with a status of ARCHIVED will be included in the response | [optional]
6652+
**summary_only** | **Boolean**| Use summaryOnly&#x3D;true in GET Contacts endpoint to retrieve a smaller version of the response object. This returns only lightweight fields, excluding computation-heavy fields from the response, making the API calls quick and efficient. | [optional] [default to false]
66506653

66516654
### Return type
66526655

0 commit comments

Comments
 (0)