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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb43e668c4cbd29f1190365df174d64a56385710319bdf3061ebe048b6dddcee
4
- data.tar.gz: 0c4bacf725b0130b1675bc300bff98840fe537958fb8eef4627c709afd69ee5c
3
+ metadata.gz: 8ce5a3437023733ca6ef290f2b874fd700ed8c4dd6d82ecba1563f88efa928f0
4
+ data.tar.gz: bda03de784931876fadd111f97836af1cdfc5a380bc3127e6f46a96991b077a7
5
5
  SHA512:
6
- metadata.gz: 0eb80a0480dbf63d28ff6295f43e5288e90c0299616bfd48a60401b8f214424baff1d990e6964bafbb76b0d569dd172ccbb151dc9c9c96a7c3697e10f4361897
7
- data.tar.gz: 780bd04d6cde627f8c6b4ac56a69c018ac9e5fef49169ccdf6a89201b6e826b253a1942e5459429256c351a459e3c0a45f105f99554aec32dadfa9b36e7fa63a
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
- ## 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.
21
14
  ## 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)
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 current gemset.
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
- ## 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
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
- 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: {..}`.
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
- ## 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.
69
63
 
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
71
66
  ```ruby
72
67
  @authorization_url = xero_client.authorization_url
73
68
 
74
69
  redirect_to @authorization_url
75
70
  ```
76
71
 
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.
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
- # 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
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
- ## 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.
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
- 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
+ =>
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
- ## 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
123
183
  ```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>
125
186
 
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)
128
189
 
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
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 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.
147
203
  remaining_connections = xero_client.disconnect(connections[0]['id'])
148
204
 
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
151
207
 
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
154
211
  ```
155
212
 
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
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
- # 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.
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
- ### NOTE
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&#x3D;1 - Up to 100 contacts will be returned in a single API call.
7730
7730
  # @option opts [Boolean] :include_archived e.g. includeArchived&#x3D;true - Contacts with a status of ARCHIVED will be included in the response
7731
+ # @option opts [Boolean] :summary_only 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. (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&#x3D;1 - Up to 100 contacts will be returned in a single API call.
7745
7746
  # @option opts [Boolean] :include_archived e.g. includeArchived&#x3D;true - Contacts with a status of ARCHIVED will be included in the response
7747
+ # @option opts [Boolean] :summary_only 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.
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?
@@ -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
- # helper to set the token_set on a client once the user
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
- set_access_token(token_set['access_token'])
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
- return token_request(data, '/token')
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['refresh_token']
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['refresh_token']
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
@@ -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.5
10
+ The version of the XeroOpenAPI document: 2.11.0
11
11
  =end
12
12
 
13
13
  module XeroRuby
14
- VERSION = '2.9.1'
14
+ VERSION = '2.10.0'
15
15
  end
@@ -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.jibberjabber', refresh_token: 'REFRESHMENTS'} }
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[:access_token])
92
- expect(api_client.access_token).to eq(token_set[:access_token])
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.9.1
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-19 00:00:00.000000000 Z
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