xero-ruby 2.9.1 → 2.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +132 -96
- data/lib/xero-ruby/api/accounting_api.rb +3 -0
- data/lib/xero-ruby/api_client.rb +50 -8
- data/lib/xero-ruby/models/projects/time_entry.rb +3 -2
- data/lib/xero-ruby/version.rb +2 -2
- data/spec/api_client_spec.rb +84 -9
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ce5a3437023733ca6ef290f2b874fd700ed8c4dd6d82ecba1563f88efa928f0
|
4
|
+
data.tar.gz: bda03de784931876fadd111f97836af1cdfc5a380bc3127e6f46a96991b077a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 472ac79286f53b8e4e7de62c5bece8e21c91a4b7b709ff86bccea9df1cedcadcfea064c01f300b6e61e6c2aa0b611a585638a7c1cb4032ba04683cc0c0f344cc
|
7
|
+
data.tar.gz: 4f6e354f10945d8cc0446042158e0e43067238f114899ef9a23d8218f17e9f5fefded2ad130e9c5ce3a850d4418a652f309bf79cd796334fd284994a05d09883
|
data/README.md
CHANGED
@@ -5,45 +5,39 @@ Xero Ruby SDK for OAuth 2.0 generated from [Xero API OpenAPI Spec](https://githu
|
|
5
5
|
|
6
6
|
# Documentation
|
7
7
|
Xero Ruby SDK supports Xero's OAuth2.0 authentication and the following Xero API sets.
|
8
|
-
|
9
|
-
## SDK Documentation
|
8
|
+
### API Client Documentation
|
10
9
|
* [API client methods](https://xeroapi.github.io/xero-ruby/accounting/index.html)
|
11
|
-
|
12
|
-
|
13
|
-
* [
|
14
|
-
|
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.
|
21
14
|
## Sample Apps
|
22
|
-
We have two apps showing SDK usage
|
23
|
-
* https://github.com/XeroAPI/xero-ruby-oauth2-starter (**Sinatra** - session based
|
24
|
-
* https://github.com/XeroAPI/xero-ruby-oauth2-app (**Rails** - token management
|
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)
|
25
18
|
|
26
19
|
![sample-app](https://i.imgur.com/OOEn55G.png)
|
27
20
|
|
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
|
+
|
29
29
|
## Installation
|
30
|
-
To install this gem to your
|
30
|
+
To install this gem to your project:
|
31
31
|
```
|
32
32
|
gem install 'xero-ruby'
|
33
33
|
```
|
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`:
|
35
35
|
```
|
36
36
|
gem 'xero-ruby'
|
37
37
|
```
|
38
38
|
|
39
|
-
|
40
|
-
|
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
|
47
41
|
```
|
48
42
|
require 'xero-ruby'
|
49
43
|
```
|
@@ -58,56 +52,77 @@ creds = {
|
|
58
52
|
xero_client ||= XeroRuby::ApiClient.new(credentials: creds)
|
59
53
|
```
|
60
54
|
|
61
|
-
|
55
|
+
For additional logging or timeout, add or override any [config](/lib/xero-ruby/configuration.rb) option by passing an optional named parameter `config: {..}`.
|
62
56
|
```ruby
|
63
57
|
config = { timeout: 30, debugging: true }
|
64
58
|
@xero_client ||= XeroRuby::ApiClient.new(credentials: creds, config: config)
|
65
59
|
```
|
66
60
|
|
67
|
-
##
|
68
|
-
All API requests require a valid access token to be set on the
|
61
|
+
## OAuth2.0 Authorization & Callback
|
62
|
+
All API requests require a valid access token to be set on the xero_client.
|
69
63
|
|
70
|
-
|
64
|
+
### Step 1
|
65
|
+
Send the user to the `authorization_url` after you have configured your xero_client
|
71
66
|
```ruby
|
72
67
|
@authorization_url = xero_client.authorization_url
|
73
68
|
|
74
69
|
redirect_to @authorization_url
|
75
70
|
```
|
76
71
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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.
|
82
79
|
```ruby
|
83
|
-
# => http://localhost:3000/oauth/callback
|
84
|
-
|
85
80
|
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.
|
86
83
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
91
96
|
```
|
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.
|
92
112
|
|
93
|
-
|
94
|
-
|
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.
|
95
116
|
```ruby
|
96
117
|
xero_client.set_token_set(user.token_set)
|
97
118
|
|
98
119
|
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.
|
100
120
|
```
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
+
=>
|
111
126
|
{
|
112
127
|
"id_token": "xxx.yyy.zz",
|
113
128
|
"access_token": "xxx.yyy.zzz",
|
@@ -116,17 +131,63 @@ Example Token set:
|
|
116
131
|
"refresh_token": "xxxxxx",
|
117
132
|
"scope": "email profile openid accounting.transactions offline_access"
|
118
133
|
}
|
119
|
-
```
|
120
134
|
|
121
|
-
|
122
|
-
|
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
|
123
183
|
```ruby
|
124
|
-
|
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>
|
125
186
|
|
126
|
-
#
|
127
|
-
|
187
|
+
# To completely Revoke a user's access token and all their connections
|
188
|
+
xero_client.revoke_token(token_set)
|
128
189
|
|
129
|
-
#
|
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
|
130
191
|
connections = xero_client.connections
|
131
192
|
[{
|
132
193
|
"id" => "xxx-yyy-zzz",
|
@@ -137,38 +198,19 @@ connections = xero_client.connections
|
|
137
198
|
"updatedDateUtc" => "2020-04-15T22:37:10.4943410"
|
138
199
|
}]
|
139
200
|
|
140
|
-
# To
|
141
|
-
#
|
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.
|
147
203
|
remaining_connections = xero_client.disconnect(connections[0]['id'])
|
148
204
|
|
149
|
-
|
150
|
-
|
205
|
+
xero_client.token_expired?
|
206
|
+
=> true || false
|
151
207
|
|
152
|
-
#
|
153
|
-
|
208
|
+
# This will check against the following logic
|
209
|
+
token_expiry = Time.at(decoded_access_token['exp'])
|
210
|
+
token_expiry < Time.now
|
154
211
|
```
|
155
212
|
|
156
|
-
|
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
|
172
214
|
### Accounting API
|
173
215
|
> https://xeroapi.github.io/xero-ruby/accounting/index.html
|
174
216
|
```ruby
|
@@ -177,7 +219,7 @@ require 'xero-ruby'
|
|
177
219
|
xero_client.refresh_token_set(user.token_set)
|
178
220
|
|
179
221
|
tenant_id = user.active_tenant_id
|
180
|
-
#
|
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.
|
181
223
|
|
182
224
|
# Get Accounts
|
183
225
|
accounts = xero_client.accounting_api.get_accounts(tenant_id).accounts
|
@@ -331,19 +373,13 @@ opts = {
|
|
331
373
|
}
|
332
374
|
xero_client.accounting_api.get_bank_transfers(tenant_id, opts).bank_transfers
|
333
375
|
```
|
334
|
-
|
376
|
+
|
335
377
|
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.
|
336
378
|
|
337
379
|
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).
|
338
380
|
* `opts = { order: 'UpdatedDateUtc DESC'}`
|
339
381
|
|
340
382
|
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
|
-
|
347
383
|
## Developing locally
|
348
384
|
To develop this gem locally against your project you can use the following development pattern:
|
349
385
|
|
@@ -7728,6 +7728,7 @@ module XeroRuby
|
|
7728
7728
|
# @option opts [Array<String>] :i_ds Filter by a comma separated list of ContactIDs. Allows you to retrieve a specific set of contacts in a single call.
|
7729
7729
|
# @option opts [Integer] :page e.g. page=1 - Up to 100 contacts will be returned in a single API call.
|
7730
7730
|
# @option opts [Boolean] :include_archived e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response
|
7731
|
+
# @option opts [Boolean] :summary_only 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. (default to false)
|
7731
7732
|
# @return [Contacts]
|
7732
7733
|
def get_contacts(xero_tenant_id, opts = {})
|
7733
7734
|
data, _status_code, _headers = get_contacts_with_http_info(xero_tenant_id, opts)
|
@@ -7743,6 +7744,7 @@ module XeroRuby
|
|
7743
7744
|
# @option opts [Array<String>] :i_ds Filter by a comma separated list of ContactIDs. Allows you to retrieve a specific set of contacts in a single call.
|
7744
7745
|
# @option opts [Integer] :page e.g. page=1 - Up to 100 contacts will be returned in a single API call.
|
7745
7746
|
# @option opts [Boolean] :include_archived e.g. includeArchived=true - Contacts with a status of ARCHIVED will be included in the response
|
7747
|
+
# @option opts [Boolean] :summary_only 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.
|
7746
7748
|
# @return [Array<(Contacts, Integer, Hash)>] Contacts data, response status code and response headers
|
7747
7749
|
def get_contacts_with_http_info(xero_tenant_id, options = {})
|
7748
7750
|
opts = options.dup
|
@@ -7766,6 +7768,7 @@ module XeroRuby
|
|
7766
7768
|
query_params[:'IDs'] = @api_client.build_collection_param(opts[:'i_ds'], :csv) if !opts[:'i_ds'].nil?
|
7767
7769
|
query_params[:'page'] = opts[:'page'] if !opts[:'page'].nil?
|
7768
7770
|
query_params[:'includeArchived'] = opts[:'include_archived'] if !opts[:'include_archived'].nil?
|
7771
|
+
query_params[:'summaryOnly'] = opts[:'summary_only'] if !opts[:'summary_only'].nil?
|
7769
7772
|
|
7770
7773
|
# XeroAPI's `IDs` convention openapi-generator does not snake_case properly.. manual over-riding `i_ds` malformations:
|
7771
7774
|
query_params[:'IDs'] = @api_client.build_collection_param(opts[:'ids'], :csv) if !opts[:'ids'].nil?
|
data/lib/xero-ruby/api_client.rb
CHANGED
@@ -17,6 +17,7 @@ require 'find'
|
|
17
17
|
require 'faraday'
|
18
18
|
require 'base64'
|
19
19
|
require 'cgi'
|
20
|
+
require 'json/jwt'
|
20
21
|
|
21
22
|
module XeroRuby
|
22
23
|
class ApiClient
|
@@ -108,17 +109,26 @@ module XeroRuby
|
|
108
109
|
@config.id_token
|
109
110
|
end
|
110
111
|
|
112
|
+
def decoded_access_token
|
113
|
+
decode_jwt(@config.access_token)
|
114
|
+
end
|
115
|
+
|
116
|
+
def decoded_id_token
|
117
|
+
decode_jwt(@config.id_token)
|
118
|
+
end
|
119
|
+
|
111
120
|
def set_token_set(token_set)
|
112
|
-
|
113
|
-
# has a valid token set ( access_token & refresh_token )
|
121
|
+
token_set = token_set.with_indifferent_access
|
114
122
|
@config.token_set = token_set
|
115
|
-
|
123
|
+
|
124
|
+
set_access_token(token_set[:access_token]) if token_set[:access_token]
|
125
|
+
set_id_token(token_set[:id_token]) if token_set[:id_token]
|
126
|
+
|
127
|
+
return true
|
116
128
|
end
|
117
129
|
|
118
130
|
def set_access_token(access_token)
|
119
|
-
# puts "access_token -> #{access_token}"
|
120
131
|
@config.access_token = access_token
|
121
|
-
# puts "@config.access_token -> #{@config.access_token}"
|
122
132
|
end
|
123
133
|
|
124
134
|
def set_id_token(id_token)
|
@@ -131,20 +141,52 @@ module XeroRuby
|
|
131
141
|
code: params['code'],
|
132
142
|
redirect_uri: @redirect_uri
|
133
143
|
}
|
134
|
-
|
144
|
+
token_set = token_request(data, '/token')
|
145
|
+
|
146
|
+
validate_tokens(token_set)
|
147
|
+
validate_state(params)
|
148
|
+
return token_set
|
149
|
+
end
|
150
|
+
|
151
|
+
def validate_tokens(token_set)
|
152
|
+
id_token = token_set[:id_token]
|
153
|
+
access_token = token_set[:access_token]
|
154
|
+
if id_token || access_token
|
155
|
+
decode_jwt(access_token) if access_token
|
156
|
+
decode_jwt(id_token) if id_token
|
157
|
+
end
|
158
|
+
return true
|
159
|
+
end
|
160
|
+
|
161
|
+
def validate_state(params)
|
162
|
+
if params[:state] != @state
|
163
|
+
raise StandardError.new "WARNING: @config.state: #{@state} and OAuth callback state: #{params['state']} do not match!"
|
164
|
+
end
|
165
|
+
return true
|
166
|
+
end
|
167
|
+
|
168
|
+
def decode_jwt(tkn)
|
169
|
+
jwks_data = JSON.parse(Faraday.get('https://identity.xero.com/.well-known/openid-configuration/jwks').body)
|
170
|
+
jwk_set = JSON::JWK::Set.new(jwks_data)
|
171
|
+
JSON::JWT.decode(tkn, jwk_set)
|
172
|
+
end
|
173
|
+
|
174
|
+
def token_expired?
|
175
|
+
token_expiry = Time.at(decoded_access_token['exp'])
|
176
|
+
token_expiry < Time.now
|
135
177
|
end
|
136
178
|
|
137
179
|
def refresh_token_set(token_set)
|
138
180
|
data = {
|
139
181
|
grant_type: 'refresh_token',
|
140
|
-
refresh_token: token_set[
|
182
|
+
refresh_token: token_set[:refresh_token]
|
141
183
|
}
|
142
184
|
return token_request(data, '/token')
|
143
185
|
end
|
144
186
|
|
145
187
|
def revoke_token(token_set)
|
146
188
|
data = {
|
147
|
-
token: token_set[
|
189
|
+
token: token_set[:refresh_token]
|
148
190
|
}
|
149
191
|
return token_request(data, '/revocation')
|
150
192
|
end
|
@@ -44,6 +44,7 @@ module XeroRuby::Projects
|
|
44
44
|
attr_accessor :status
|
45
45
|
ACTIVE = "ACTIVE".freeze
|
46
46
|
LOCKED = "LOCKED".freeze
|
47
|
+
INVOICED = "INVOICED".freeze
|
47
48
|
|
48
49
|
class EnumAttributeValidator
|
49
50
|
attr_reader :datatype
|
@@ -159,7 +160,7 @@ module XeroRuby::Projects
|
|
159
160
|
# Check to see if the all the properties in the model are valid
|
160
161
|
# @return true if the model is valid
|
161
162
|
def valid?
|
162
|
-
status_validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED"])
|
163
|
+
status_validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED", "INVOICED"])
|
163
164
|
return false unless status_validator.valid?(@status)
|
164
165
|
true
|
165
166
|
end
|
@@ -167,7 +168,7 @@ module XeroRuby::Projects
|
|
167
168
|
# Custom attribute writer method checking allowed values (enum).
|
168
169
|
# @param [Object] status Object to be assigned
|
169
170
|
def status=(status)
|
170
|
-
validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED"])
|
171
|
+
validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED", "INVOICED"])
|
171
172
|
unless validator.valid?(status)
|
172
173
|
fail ArgumentError, "invalid value for \"status\", must be one of #{validator.allowable_values}."
|
173
174
|
end
|
data/lib/xero-ruby/version.rb
CHANGED
@@ -7,9 +7,9 @@ Contact: api@xero.com
|
|
7
7
|
Generated by: https://openapi-generator.tech
|
8
8
|
OpenAPI Generator version: 4.3.1
|
9
9
|
|
10
|
-
The version of the XeroOpenAPI document: 2.
|
10
|
+
The version of the XeroOpenAPI document: 2.11.0
|
11
11
|
=end
|
12
12
|
|
13
13
|
module XeroRuby
|
14
|
-
VERSION = '2.
|
14
|
+
VERSION = '2.10.0'
|
15
15
|
end
|
data/spec/api_client_spec.rb
CHANGED
@@ -60,13 +60,26 @@ describe XeroRuby::ApiClient do
|
|
60
60
|
api_client = XeroRuby::ApiClient.new(credentials: creds)
|
61
61
|
expect(api_client.authorization_url).to eq('https://login.xero.com/identity/connect/authorize?response_type=code&client_id=abc&redirect_uri=https://mydomain.com/callback&scope=openid+profile+email+accounting.transactions+accounting.settings')
|
62
62
|
end
|
63
|
+
|
64
|
+
it "Validates state on callback matches @config.state" do
|
65
|
+
creds = {
|
66
|
+
client_id: 'abc',
|
67
|
+
client_secret: '123',
|
68
|
+
redirect_uri: 'https://mydomain.com/callback',
|
69
|
+
scopes: 'openid profile email accounting.transactions accounting.settings',
|
70
|
+
state: "custom-state"
|
71
|
+
}
|
72
|
+
api_client = XeroRuby::ApiClient.new(credentials: creds)
|
73
|
+
altered_state = {'state': 'not-original-state'}
|
74
|
+
expect{api_client.validate_state(altered_state)}.to raise_error(StandardError, 'WARNING: @config.state: custom-state and OAuth callback state: do not match!')
|
75
|
+
end
|
63
76
|
end
|
64
77
|
end
|
65
78
|
end
|
66
79
|
|
67
80
|
describe 'api_client helper functions' do
|
68
81
|
let(:api_client) { XeroRuby::ApiClient.new }
|
69
|
-
let(:token_set) { {access_token: 'eyx.
|
82
|
+
let(:token_set) { {'access_token': 'eyx.authorization.data', 'id_token': 'eyx.authentication.data', 'refresh_token': 'REFRESHMENTS'} }
|
70
83
|
let(:connections) {
|
71
84
|
[{
|
72
85
|
"id" => "xxx-yyy-zzz",
|
@@ -84,12 +97,17 @@ describe XeroRuby::ApiClient do
|
|
84
97
|
|
85
98
|
it "#set_token_set" do
|
86
99
|
api_client.set_token_set(token_set)
|
87
|
-
expect(api_client.token_set).to eq(token_set)
|
100
|
+
expect(api_client.token_set).to eq(token_set.with_indifferent_access)
|
88
101
|
end
|
89
102
|
|
90
103
|
it "#set_access_token" do
|
91
|
-
api_client.set_access_token(token_set[
|
92
|
-
expect(api_client.access_token).to eq(token_set[
|
104
|
+
api_client.set_access_token(token_set['access_token'])
|
105
|
+
expect(api_client.access_token).to eq(token_set['access_token'])
|
106
|
+
end
|
107
|
+
|
108
|
+
it "#set_id_token" do
|
109
|
+
api_client.set_id_token(token_set['id_token'])
|
110
|
+
expect(api_client.id_token).to eq(token_set['id_token'])
|
93
111
|
end
|
94
112
|
|
95
113
|
it "#refresh_token_set" do
|
@@ -372,6 +390,63 @@ describe XeroRuby::ApiClient do
|
|
372
390
|
end
|
373
391
|
end
|
374
392
|
|
393
|
+
describe 'token helper methods' do
|
394
|
+
let(:api_client) { XeroRuby::ApiClient.new }
|
395
|
+
let(:id_token){'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFDQUY4RTY2NzcyRDZEQzAyOEQ2NzI2RkQwMjYxNTgxNTcwRUZDMTkiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJISy1PWm5jdGJjQW8xbkp2MENZVmdWY09fQmsifQ.eyJuYmYiOjE2MTk3MTQwNDMsImV4cCI6MTYxOTcxNDM0MywiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS54ZXJvLmNvbSIsImF1ZCI6IkFEQjVBNzdEQTZCNjRFOTI4RDg0MDkwOTlBMzlDQTdCIiwiaWF0IjoxNjE5NzE0MDQzLCJhdF9oYXNoIjoiMXJNamVvUTJiOUxUNFU0ZlBXbEZJZyIsInNpZCI6ImY0YTY4ZDc0ZmM3OTQzMjc4YTgzMTg0NGM5ZWRmNzFiIiwic3ViIjoiZGI0ZjBmMzdiNTg1NTMwZTkxZjNiOWNiYjUwMzQwZTgiLCJhdXRoX3RpbWUiOjE2MTk3MTM5ODcsInhlcm9fdXNlcmlkIjoiZmFhODNlYzktZjZhNy00ODlmLTg5MTEtZTNmY2UwM2ExMTg2IiwiZ2xvYmFsX3Nlc3Npb25faWQiOiJmNGE2OGQ3NGZjNzk0MzI3OGE4MzE4NDRjOWVkZjcxYiIsInByZWZlcnJlZF91c2VybmFtZSI6ImNocmlzLmtuaWdodEB4ZXJvLmNvbSIsImVtYWlsIjoiY2hyaXMua25pZ2h0QHhlcm8uY29tIiwiZ2l2ZW5fbmFtZSI6IkNocmlzdG9waGVyIiwiZmFtaWx5X25hbWUiOiJLbmlnaHQifQ.hF04tCE1Qd-al355fQyCjWqTVWKnguor4RD1sC7rKH7zV3r3_nGwnGLMm2A96fov06fig0zusTX8onev0qFLZy-jlEXDp1f19LHhT15sBy0KH6dB0lGMrM14BnDuEP4NUGeP06nAPhQHHLw2oCc9hzYXorRVOSFDw43jgAC0vxRgDvJwgKgv6TDVEmpvwP0S4R7A0VbnFemHP_HY8gLHd7RpN7rrYmpJC4cofztdptDNLTF8Qup8qVlFdQgpJPQEQ95N1m6W-unvrh_dlO6AVMjXBjC1BJ10IGzoCCr8DSVyz2UMPnUT3oIYFVTlDc2K-ZJYkW86pigITMCdvR1hKg'}
|
396
|
+
let(:access_token){'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFDQUY4RTY2NzcyRDZEQzAyOEQ2NzI2RkQwMjYxNTgxNTcwRUZDMTkiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJISy1PWm5jdGJjQW8xbkp2MENZVmdWY09fQmsifQ.eyJuYmYiOjE2MTk3MTQwNDMsImV4cCI6MTYxOTcxNTg0MywiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS54ZXJvLmNvbSIsImF1ZCI6Imh0dHBzOi8vaWRlbnRpdHkueGVyby5jb20vcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoiQURCNUE3N0RBNkI2NEU5MjhEODQwOTA5OUEzOUNBN0IiLCJzdWIiOiJkYjRmMGYzN2I1ODU1MzBlOTFmM2I5Y2JiNTAzNDBlOCIsImF1dGhfdGltZSI6MTYxOTcxMzk4NywieGVyb191c2VyaWQiOiJmYWE4M2VjOS1mNmE3LTQ4OWYtODkxMS1lM2ZjZTAzYTExODYiLCJnbG9iYWxfc2Vzc2lvbl9pZCI6ImY0YTY4ZDc0ZmM3OTQzMjc4YTgzMTg0NGM5ZWRmNzFiIiwianRpIjoiZmFmNGNkYzQ5MjM0YzhmZDE0OTA0ZjRlOWEyMWY4YmYiLCJhdXRoZW50aWNhdGlvbl9ldmVudF9pZCI6IjI0MmRjNWIyLTIwZTMtNGFjNS05NjU3LWExMGI5ZTI0ZGI1NSIsInNjb3BlIjpbImVtYWlsIiwicHJvZmlsZSIsIm9wZW5pZCIsImFjY291bnRpbmcucmVwb3J0cy5yZWFkIiwiZmlsZXMiLCJwYXlyb2xsLmVtcGxveWVlcyIsInBheXJvbGwucGF5cnVucyIsInBheXJvbGwucGF5c2xpcCIsInBheXJvbGwudGltZXNoZWV0cyIsInByb2plY3RzLnJlYWQiLCJwcm9qZWN0cyIsImFjY291bnRpbmcuc2V0dGluZ3MiLCJhY2NvdW50aW5nLmF0dGFjaG1lbnRzIiwiYWNjb3VudGluZy50cmFuc2FjdGlvbnMiLCJhY2NvdW50aW5nLmpvdXJuYWxzLnJlYWQiLCJhc3NldHMucmVhZCIsImFzc2V0cyIsImFjY291bnRpbmcuY29udGFjdHMiLCJwYXlyb2xsLnNldHRpbmdzIiwib2ZmbGluZV9hY2Nlc3MiXX0.vNV-YsgHFWKFBmyYdhg7tztdsPc9ykObadQcGFoFXJ8qCBerR3h7XXKzWAP3KzFzhOCcIpWU8Q081zuYBNxahPeeLRLUuc_3MwgwE72esE5vGuxa2_-_QidtNvMCgsX-ie_LcX7FE_KI-sXB_EZ8fDk6WAMIPC9d3GejgeuH5Uh6rZkhowN2jm5pZjEOEy_QE7PScBO0XEbiZNUsarvBUSdKuSTvVVLHzHzs0bHMRfgKEkqZySNtZlac-oyaL3PVba1S7A_vbRcNWpYR_VrKGf2g9LHSI3EA5j3Beto4pKukU-bk6rLBGul37u4tM17U-wyJLsFmt6ZC_SEJKgmluQ'}
|
397
|
+
let(:tkn_set) {{'id_token': id_token, 'access_token': access_token, 'refresh_token': 'abc123xyz'}}
|
398
|
+
|
399
|
+
before do
|
400
|
+
api_client.set_token_set(tkn_set)
|
401
|
+
end
|
402
|
+
|
403
|
+
it '#token_expired? for an expired token' do
|
404
|
+
expect(api_client.token_expired?).to eq(true)
|
405
|
+
end
|
406
|
+
|
407
|
+
it '#token_expired? for a just expired token' do
|
408
|
+
allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>Time.now.to_i})
|
409
|
+
expect(api_client.token_expired?).to eq(true)
|
410
|
+
end
|
411
|
+
|
412
|
+
it '#token_expired? for a non-expired token' do
|
413
|
+
allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>(Time.now + 30.minutes).to_i})
|
414
|
+
expect(api_client.token_expired?).to eq(false)
|
415
|
+
end
|
416
|
+
|
417
|
+
it '#token_expired? for an almost expired token' do
|
418
|
+
allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>(Time.now + 30.seconds).to_i})
|
419
|
+
expect(api_client.token_expired?).to eq(false)
|
420
|
+
end
|
421
|
+
|
422
|
+
it '#validate_tokens' do
|
423
|
+
expect(api_client.validate_tokens(tkn_set)).to eq(true)
|
424
|
+
end
|
425
|
+
it '#access_token' do
|
426
|
+
expect(api_client.access_token).to eq(access_token)
|
427
|
+
end
|
428
|
+
it '#decoded_access_token' do
|
429
|
+
expect(api_client.decoded_access_token['aud']).to eq("https://identity.xero.com/resources")
|
430
|
+
end
|
431
|
+
it '#id_token' do
|
432
|
+
expect(api_client.id_token).to eq(tkn_set[:id_token])
|
433
|
+
end
|
434
|
+
it '#decoded_id_token' do
|
435
|
+
expect(api_client.decoded_id_token['email']).to eq('chris.knight@xero.com')
|
436
|
+
end
|
437
|
+
|
438
|
+
it 'decoding an invalid access_token' do
|
439
|
+
api_client.set_access_token("#{access_token}.NotAValidJWTstring")
|
440
|
+
expect{api_client.decoded_access_token}.to raise_error(JSON::JWT::InvalidFormat)
|
441
|
+
end
|
442
|
+
|
443
|
+
it 'decoding an invalid id_token' do
|
444
|
+
api_client.set_id_token("#{id_token}.NotAValidJWTstring")
|
445
|
+
expect{api_client.decoded_id_token}.to raise_error(JSON::JWT::InvalidFormat)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
|
375
450
|
describe 'thread safety in the XeroClient' do
|
376
451
|
let(:creds) {{
|
377
452
|
client_id: 'abc',
|
@@ -383,8 +458,8 @@ describe XeroRuby::ApiClient do
|
|
383
458
|
let(:api_client_2) {XeroRuby::ApiClient.new(credentials: creds)}
|
384
459
|
let(:api_client_3) {XeroRuby::ApiClient.new(credentials: creds)}
|
385
460
|
|
386
|
-
let(:tkn_set_1){{id_token: "abc.123.1", access_token: "xxx.yyy.zzz.111"}}
|
387
|
-
let(:tkn_set_2){{id_token: "efg.456.2", access_token: "xxx.yyy.zzz.222"}}
|
461
|
+
let(:tkn_set_1){{'id_token': "abc.123.1", 'access_token': "xxx.yyy.zzz.111"}}
|
462
|
+
let(:tkn_set_2){{'id_token': "efg.456.2", 'access_token': "xxx.yyy.zzz.222"}}
|
388
463
|
|
389
464
|
describe 'when configuration is changed, other instantiations of the client are not affected' do
|
390
465
|
it 'applies to #set_access_token' do
|
@@ -426,12 +501,12 @@ describe XeroRuby::ApiClient do
|
|
426
501
|
expect(api_client_2.token_set).to eq(nil)
|
427
502
|
|
428
503
|
api_client_1.set_token_set(tkn_set_1)
|
429
|
-
expect(api_client_1.token_set).to eq(tkn_set_1)
|
504
|
+
expect(api_client_1.token_set).to eq(tkn_set_1.with_indifferent_access)
|
430
505
|
expect(api_client_2.token_set).to eq(nil)
|
431
506
|
|
432
507
|
api_client_2.set_token_set(tkn_set_2)
|
433
|
-
expect(api_client_1.token_set).to eq(tkn_set_1)
|
434
|
-
expect(api_client_2.token_set).to eq(tkn_set_2)
|
508
|
+
expect(api_client_1.token_set).to eq(tkn_set_1.with_indifferent_access)
|
509
|
+
expect(api_client_2.token_set).to eq(tkn_set_2.with_indifferent_access)
|
435
510
|
end
|
436
511
|
|
437
512
|
it 'applies to #base_url' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xero-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xero API Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -50,6 +50,26 @@ dependencies:
|
|
50
50
|
- - ">="
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: 2.1.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: json-jwt
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.5'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.5.2
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.5'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 1.5.2
|
53
73
|
- !ruby/object:Gem::Dependency
|
54
74
|
name: rspec
|
55
75
|
requirement: !ruby/object:Gem::Requirement
|