workos 1.3.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/Gemfile.lock +3 -3
  4. data/lib/workos/client.rb +3 -6
  5. data/lib/workos/connection.rb +9 -1
  6. data/lib/workos/directory.rb +9 -1
  7. data/lib/workos/directory_user.rb +4 -1
  8. data/lib/workos/errors.rb +4 -0
  9. data/lib/workos/organization.rb +10 -1
  10. data/lib/workos/organizations.rb +18 -4
  11. data/lib/workos/profile.rb +7 -2
  12. data/lib/workos/types/connection_struct.rb +2 -0
  13. data/lib/workos/types/directory_struct.rb +2 -0
  14. data/lib/workos/types/directory_user_struct.rb +1 -0
  15. data/lib/workos/types/organization_struct.rb +3 -0
  16. data/lib/workos/types/profile_struct.rb +1 -0
  17. data/lib/workos/types/provider_enum.rb +1 -0
  18. data/lib/workos/types/webhook_struct.rb +14 -0
  19. data/lib/workos/types.rb +1 -0
  20. data/lib/workos/version.rb +1 -1
  21. data/lib/workos/webhook.rb +47 -0
  22. data/lib/workos/webhooks.rb +168 -0
  23. data/lib/workos.rb +3 -0
  24. data/spec/lib/workos/audit_trail_spec.rb +2 -0
  25. data/spec/lib/workos/directory_sync_spec.rb +21 -19
  26. data/spec/lib/workos/organizations_spec.rb +13 -11
  27. data/spec/lib/workos/passwordless_spec.rb +2 -0
  28. data/spec/lib/workos/portal_spec.rb +2 -0
  29. data/spec/lib/workos/sso_spec.rb +17 -13
  30. data/spec/lib/workos/webhooks_spec.rb +190 -0
  31. data/spec/spec_helper.rb +3 -0
  32. data/spec/support/fixtures/vcr_cassettes/directory_sync/get_user.yml +40 -16
  33. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_after.yml +3 -3
  34. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_before.yml +3 -3
  35. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_domain.yml +1 -1
  36. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_limit.yml +2 -2
  37. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_no_options.yml +3 -3
  38. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_directories/with_search.yml +1 -1
  39. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_after.yml +128 -28
  40. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_before.yml +31 -18
  41. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_directory.yml +136 -35
  42. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_group.yml +128 -18
  43. data/spec/support/fixtures/vcr_cassettes/directory_sync/list_users/with_limit.yml +131 -17
  44. data/spec/support/fixtures/vcr_cassettes/organization/create.yml +28 -16
  45. data/spec/support/fixtures/vcr_cassettes/organization/get.yml +27 -16
  46. data/spec/support/fixtures/vcr_cassettes/organization/list.yml +29 -14
  47. data/spec/support/fixtures/vcr_cassettes/organization/update.yml +27 -16
  48. data/spec/support/fixtures/vcr_cassettes/sso/get_connection_with_valid_id.yml +28 -16
  49. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_after.yml +25 -15
  50. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_before.yml +28 -15
  51. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_connection_type.yml +31 -14
  52. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_domain.yml +27 -13
  53. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_limit.yml +24 -15
  54. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_no_options.yml +30 -14
  55. data/spec/support/fixtures/vcr_cassettes/sso/list_connections/with_organization_id.yml +28 -14
  56. data/spec/support/fixtures/vcr_cassettes/sso/profile.yml +1 -1
  57. data/spec/support/profile.txt +1 -1
  58. data/spec/support/shared_examples/client_spec.rb +16 -0
  59. data/spec/support/webhook_payload.txt +1 -0
  60. metadata +12 -5
  61. data/spec/support/fixtures/vcr_cassettes/organization/update_invalid.yml +0 -73
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::DirectorySync do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.list_directories' do
6
8
  context 'with no options' do
7
9
  it 'returns directories and metadata' do
@@ -282,7 +284,7 @@ describe WorkOS::DirectorySync do
282
284
  context 'with directory option' do
283
285
  it 'forms the proper request to the API' do
284
286
  request_args = [
285
- '/directory_users?directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
287
+ '/directory_users?directory=directory_01FAZYMST676QMTFN1DDJZZX87',
286
288
  'Content-Type' => 'application/json'
287
289
  ]
288
290
 
@@ -293,10 +295,10 @@ describe WorkOS::DirectorySync do
293
295
 
294
296
  VCR.use_cassette 'directory_sync/list_users/with_directory' do
295
297
  users = described_class.list_users(
296
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
298
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
297
299
  )
298
300
 
299
- expect(users.data.size).to eq(10)
301
+ expect(users.data.size).to eq(4)
300
302
  end
301
303
  end
302
304
  end
@@ -304,7 +306,7 @@ describe WorkOS::DirectorySync do
304
306
  context 'with group option' do
305
307
  it 'forms the proper request to the API' do
306
308
  request_args = [
307
- '/directory_users?group=directory_group_01EQ7V7C6Y4RPMCH3KNB9853FF',
309
+ '/directory_users?group=directory_group_01FBXGP79EJAYKW0WS9JCK1V6E',
308
310
  'Content-Type' => 'application/json'
309
311
  ]
310
312
 
@@ -315,10 +317,10 @@ describe WorkOS::DirectorySync do
315
317
 
316
318
  VCR.use_cassette 'directory_sync/list_users/with_group' do
317
319
  users = described_class.list_users(
318
- group: 'directory_group_01EQ7V7C6Y4RPMCH3KNB9853FF',
320
+ group: 'directory_group_01FBXGP79EJAYKW0WS9JCK1V6E',
319
321
  )
320
322
 
321
- expect(users.data.size).to eq(2)
323
+ expect(users.data.size).to eq(1)
322
324
  end
323
325
  end
324
326
  end
@@ -326,8 +328,8 @@ describe WorkOS::DirectorySync do
326
328
  context 'with the before option' do
327
329
  it 'forms the proper request to the API' do
328
330
  request_args = [
329
- '/directory_users?before=before-id&'\
330
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
331
+ '/directory_users?before=directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF&'\
332
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
331
333
  'Content-Type' => 'application/json'
332
334
  ]
333
335
 
@@ -338,8 +340,8 @@ describe WorkOS::DirectorySync do
338
340
 
339
341
  VCR.use_cassette 'directory_sync/list_users/with_before' do
340
342
  users = described_class.list_users(
341
- before: 'before-id',
342
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
343
+ before: 'directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF',
344
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
343
345
  )
344
346
 
345
347
  expect(users.data.size).to eq(2)
@@ -350,8 +352,8 @@ describe WorkOS::DirectorySync do
350
352
  context 'with the after option' do
351
353
  it 'forms the proper request to the API' do
352
354
  request_args = [
353
- '/directory_users?after=after-id&' \
354
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
355
+ '/directory_users?after=directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF&' \
356
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
355
357
  'Content-Type' => 'application/json'
356
358
  ]
357
359
 
@@ -362,11 +364,11 @@ describe WorkOS::DirectorySync do
362
364
 
363
365
  VCR.use_cassette 'directory_sync/list_users/with_after' do
364
366
  users = described_class.list_users(
365
- after: 'after-id',
366
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
367
+ after: 'directory_user_01FAZYNPC8TJBP7Y2ERT51MGDF',
368
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
367
369
  )
368
370
 
369
- expect(users.data.size).to eq(10)
371
+ expect(users.data.size).to eq(1)
370
372
  end
371
373
  end
372
374
  end
@@ -375,7 +377,7 @@ describe WorkOS::DirectorySync do
375
377
  it 'forms the proper request to the API' do
376
378
  request_args = [
377
379
  '/directory_users?limit=2&' \
378
- 'directory=directory_01EK2YEMVTWGX27STRDR0N3MP9',
380
+ 'directory=directory_01FAZYMST676QMTFN1DDJZZX87',
379
381
  'Content-Type' => 'application/json'
380
382
  ]
381
383
 
@@ -387,7 +389,7 @@ describe WorkOS::DirectorySync do
387
389
  VCR.use_cassette 'directory_sync/list_users/with_limit' do
388
390
  users = described_class.list_users(
389
391
  limit: 2,
390
- directory: 'directory_01EK2YEMVTWGX27STRDR0N3MP9',
392
+ directory: 'directory_01FAZYMST676QMTFN1DDJZZX87',
391
393
  )
392
394
 
393
395
  expect(users.data.size).to eq(2)
@@ -425,10 +427,10 @@ describe WorkOS::DirectorySync do
425
427
  it 'returns a user' do
426
428
  VCR.use_cassette('directory_sync/get_user') do
427
429
  user = WorkOS::DirectorySync.get_user(
428
- 'directory_usr_01E64QS50EAY48S0XJ1AA4WX4D',
430
+ 'directory_user_01FAZYNPC8M0HRYTKFP2GNX852',
429
431
  )
430
432
 
431
- expect(user['first_name']).to eq('Mark')
433
+ expect(user['first_name']).to eq('Logan')
432
434
  end
433
435
  end
434
436
  end
@@ -2,18 +2,20 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Organizations do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.create_organization' do
6
8
  context 'with valid payload' do
7
9
  it 'creates an organization' do
8
10
  VCR.use_cassette 'organization/create' do
9
11
  organization = described_class.create_organization(
10
- domains: ['example.com'],
12
+ domains: ['example.io'],
11
13
  name: 'Test Organization',
12
14
  )
13
15
 
14
- expect(organization.id).to eq('org_01EHT88Z8J8795GZNQ4ZP1J81T')
16
+ expect(organization.id).to eq('org_01FCPEJXEZR4DSBA625YMGQT9N')
15
17
  expect(organization.name).to eq('Test Organization')
16
- expect(organization.domains.first[:domain]).to eq('example.com')
18
+ expect(organization.domains.first[:domain]).to eq('example.io')
17
19
  end
18
20
  end
19
21
  end
@@ -46,7 +48,7 @@ describe WorkOS::Organizations do
46
48
  VCR.use_cassette 'organization/list' do
47
49
  organizations = described_class.list_organizations
48
50
 
49
- expect(organizations.data.size).to eq(7)
51
+ expect(organizations.data.size).to eq(6)
50
52
  expect(organizations.list_metadata).to eq(expected_metadata)
51
53
  end
52
54
  end
@@ -69,7 +71,7 @@ describe WorkOS::Organizations do
69
71
  before: 'before-id',
70
72
  )
71
73
 
72
- expect(organizations.data.size).to eq(7)
74
+ expect(organizations.data.size).to eq(6)
73
75
  end
74
76
  end
75
77
  end
@@ -89,7 +91,7 @@ describe WorkOS::Organizations do
89
91
  VCR.use_cassette 'organization/list', match_requests_on: [:path] do
90
92
  organizations = described_class.list_organizations(after: 'after-id')
91
93
 
92
- expect(organizations.data.size).to eq(7)
94
+ expect(organizations.data.size).to eq(6)
93
95
  end
94
96
  end
95
97
  end
@@ -109,7 +111,7 @@ describe WorkOS::Organizations do
109
111
  VCR.use_cassette 'organization/list', match_requests_on: [:path] do
110
112
  organizations = described_class.list_organizations(limit: 10)
111
113
 
112
- expect(organizations.data.size).to eq(7)
114
+ expect(organizations.data.size).to eq(6)
113
115
  end
114
116
  end
115
117
  end
@@ -120,10 +122,10 @@ describe WorkOS::Organizations do
120
122
  it 'gets the organization details' do
121
123
  VCR.use_cassette('organization/get') do
122
124
  organization = described_class.get_organization(
123
- id: 'org_01EZDF20TZEJXKPSX2BJRN6TV6',
125
+ id: 'org_01F9293WD2PDEEV4Y625XPZVG7',
124
126
  )
125
127
 
126
- expect(organization.id).to eq('org_01EZDF20TZEJXKPSX2BJRN6TV6')
128
+ expect(organization.id).to eq('org_01F9293WD2PDEEV4Y625XPZVG7')
127
129
  expect(organization.name).to eq('Foo Corp')
128
130
  expect(organization.domains.first[:domain]).to eq('foo-corp.com')
129
131
  end
@@ -149,12 +151,12 @@ describe WorkOS::Organizations do
149
151
  it 'creates an organization' do
150
152
  VCR.use_cassette 'organization/update' do
151
153
  organization = described_class.update_organization(
152
- organization: 'org_01F29YJ068E52HGEB8ZQGC9MJG',
154
+ organization: 'org_01F6Q6TFP7RD2PF6J03ANNWDKV',
153
155
  domains: ['example.me'],
154
156
  name: 'Test Organization',
155
157
  )
156
158
 
157
- expect(organization.id).to eq('org_01F29YJ068E52HGEB8ZQGC9MJG')
159
+ expect(organization.id).to eq('org_01F6Q6TFP7RD2PF6J03ANNWDKV')
158
160
  expect(organization.name).to eq('Test Organization')
159
161
  expect(organization.domains.first[:domain]).to eq('example.me')
160
162
  end
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Passwordless do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.create_session' do
6
8
  context 'with valid options payload' do
7
9
  let(:valid_options) do
@@ -2,6 +2,8 @@
2
2
  # typed: false
3
3
 
4
4
  describe WorkOS::Portal do
5
+ it_behaves_like 'client'
6
+
5
7
  describe '.generate_link' do
6
8
  let(:organization) { 'org_01EHQMYV6MBK39QC5PZXHY59C3' }
7
9
 
@@ -4,6 +4,8 @@
4
4
  require 'securerandom'
5
5
 
6
6
  describe WorkOS::SSO do
7
+ it_behaves_like 'client'
8
+
7
9
  describe '.authorization_url' do
8
10
  context 'with a domain' do
9
11
  let(:args) do
@@ -143,7 +145,7 @@ describe WorkOS::SSO do
143
145
  described_class.authorization_url(**args)
144
146
  end.to raise_error(
145
147
  ArgumentError,
146
- 'Okta is not a valid value. `provider` must be in ["GoogleOAuth"]',
148
+ 'Okta is not a valid value. `provider` must be in ["GoogleOAuth", "MicrosoftOAuth"]',
147
149
  )
148
150
  end
149
151
  end
@@ -162,6 +164,7 @@ describe WorkOS::SSO do
162
164
  id: 'prof_01EEJTY9SZ1R350RB7B73SNBKF',
163
165
  idp_id: '116485463307139932699',
164
166
  last_name: 'Loblaw',
167
+ organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
165
168
  raw_attributes: {
166
169
  email: 'bob.loblaw@workos.com',
167
170
  family_name: 'Loblaw',
@@ -231,6 +234,7 @@ describe WorkOS::SSO do
231
234
  id: 'prof_01DRA1XNSJDZ19A31F183ECQW5',
232
235
  idp_id: '00u1klkowm8EGah2H357',
233
236
  last_name: 'Demo',
237
+ organization_id: 'org_01FG53X8636WSNW2WEKB2C31ZB',
234
238
  raw_attributes: {
235
239
  email: 'demo@workos-okta.com',
236
240
  first_name: 'WorkOS',
@@ -303,7 +307,7 @@ describe WorkOS::SSO do
303
307
  VCR.use_cassette 'sso/list_connections/with_no_options' do
304
308
  connections = described_class.list_connections
305
309
 
306
- expect(connections.data.size).to eq(3)
310
+ expect(connections.data.size).to eq(6)
307
311
  expect(connections.list_metadata).to eq(expected_metadata)
308
312
  end
309
313
  end
@@ -326,7 +330,7 @@ describe WorkOS::SSO do
326
330
  connection_type: 'OktaSAML',
327
331
  )
328
332
 
329
- expect(connections.data.size).to eq(3)
333
+ expect(connections.data.size).to eq(10)
330
334
  expect(connections.data.first.connection_type).to eq('OktaSAML')
331
335
  end
332
336
  end
@@ -357,7 +361,7 @@ describe WorkOS::SSO do
357
361
  context 'with organization_id option' do
358
362
  it 'forms the proper request to the API' do
359
363
  request_args = [
360
- '/connections?organization_id=org_01EGS4P7QR31EZ4YWD1Z1XA176',
364
+ '/connections?organization_id=org_01F9293WD2PDEEV4Y625XPZVG7',
361
365
  'Content-Type' => 'application/json'
362
366
  ]
363
367
 
@@ -368,12 +372,12 @@ describe WorkOS::SSO do
368
372
 
369
373
  VCR.use_cassette 'sso/list_connections/with_organization_id' do
370
374
  connections = described_class.list_connections(
371
- organization_id: 'org_01EGS4P7QR31EZ4YWD1Z1XA176',
375
+ organization_id: 'org_01F9293WD2PDEEV4Y625XPZVG7',
372
376
  )
373
377
 
374
378
  expect(connections.data.size).to eq(1)
375
379
  expect(connections.data.first.organization_id).to eq(
376
- 'org_01EGS4P7QR31EZ4YWD1Z1XA176',
380
+ 'org_01F9293WD2PDEEV4Y625XPZVG7',
377
381
  )
378
382
  end
379
383
  end
@@ -404,7 +408,7 @@ describe WorkOS::SSO do
404
408
  context 'with before option' do
405
409
  it 'forms the proper request to the API' do
406
410
  request_args = [
407
- '/connections?before=conn_01EQKPMQAPV02H270HKVNS4CTA',
411
+ '/connections?before=conn_01FA3WGCWPCCY1V2FGES2FDNP7',
408
412
  'Content-Type' => 'application/json'
409
413
  ]
410
414
 
@@ -415,7 +419,7 @@ describe WorkOS::SSO do
415
419
 
416
420
  VCR.use_cassette 'sso/list_connections/with_before' do
417
421
  connections = described_class.list_connections(
418
- before: 'conn_01EQKPMQAPV02H270HKVNS4CTA',
422
+ before: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
419
423
  )
420
424
 
421
425
  expect(connections.data.size).to eq(3)
@@ -426,7 +430,7 @@ describe WorkOS::SSO do
426
430
  context 'with after option' do
427
431
  it 'forms the proper request to the API' do
428
432
  request_args = [
429
- '/connections?after=conn_01EQKPMQAPV02H270HKVNS4CTA',
433
+ '/connections?after=conn_01FA3WGCWPCCY1V2FGES2FDNP7',
430
434
  'Content-Type' => 'application/json'
431
435
  ]
432
436
 
@@ -437,10 +441,10 @@ describe WorkOS::SSO do
437
441
 
438
442
  VCR.use_cassette 'sso/list_connections/with_after' do
439
443
  connections = described_class.list_connections(
440
- after: 'conn_01EQKPMQAPV02H270HKVNS4CTA',
444
+ after: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
441
445
  )
442
446
 
443
- expect(connections.data.size).to eq(3)
447
+ expect(connections.data.size).to eq(2)
444
448
  end
445
449
  end
446
450
  end
@@ -451,10 +455,10 @@ describe WorkOS::SSO do
451
455
  it 'gets the connection details' do
452
456
  VCR.use_cassette('sso/get_connection_with_valid_id') do
453
457
  connection = WorkOS::SSO.get_connection(
454
- id: 'conn_01EX00NB050H354WKGC7990AR2',
458
+ id: 'conn_01FA3WGCWPCCY1V2FGES2FDNP7',
455
459
  )
456
460
 
457
- expect(connection.id).to eq('conn_01EX00NB050H354WKGC7990AR2')
461
+ expect(connection.id).to eq('conn_01FA3WGCWPCCY1V2FGES2FDNP7')
458
462
  expect(connection.connection_type).to eq('OktaSAML')
459
463
  expect(connection.name).to eq('Foo Corp')
460
464
  expect(connection.domains.first[:domain]).to eq('foo-corp.com')
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require 'json'
5
+ require 'openssl'
6
+
7
+ describe WorkOS::Webhooks do
8
+ describe '.construct_event' do
9
+ before(:each) do
10
+ @payload = File.read("#{SPEC_ROOT}/support/webhook_payload.txt")
11
+ @secret = 'secret'
12
+ @timestamp = Time.at(Time.now.to_i * 1000)
13
+ unhashed_string = "#{@timestamp.to_i}.#{@payload}"
14
+ digest = OpenSSL::Digest.new('sha256')
15
+ @signature_hash = OpenSSL::HMAC.hexdigest(digest, @secret, unhashed_string)
16
+ @expectation = {
17
+ id: 'directory_user_01FAEAJCR3ZBZ30D8BD1924TVG',
18
+ state: 'active',
19
+ emails: [{
20
+ type: 'work',
21
+ value: 'blair@foo-corp.com',
22
+ primary: true,
23
+ }],
24
+ idp_id: '00u1e8mutl6wlH3lL4x7',
25
+ object: 'directory_user',
26
+ username: 'blair@foo-corp.com',
27
+ last_name: 'Lunceford',
28
+ first_name: 'Blair',
29
+ directory_id: 'directory_01F9M7F68PZP8QXP8G7X5QRHS7',
30
+ raw_attributes: {
31
+ name: {
32
+ givenName: 'Blair',
33
+ familyName: 'Lunceford',
34
+ middleName: 'Elizabeth',
35
+ honorificPrefix: 'Ms.',
36
+ },
37
+ title: 'Developer Success Engineer',
38
+ active: true,
39
+ emails: [{
40
+ type: 'work',
41
+ value: 'blair@foo-corp.com',
42
+ primary: true,
43
+ }],
44
+ groups: [],
45
+ locale: 'en-US',
46
+ schemas: [
47
+ 'urn:ietf:params:scim:schemas:core:2.0:User',
48
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
49
+ ],
50
+ userName: 'blair@foo-corp.com',
51
+ addresses: [{
52
+ region: 'CO',
53
+ primary: true,
54
+ locality: 'Steamboat Springs',
55
+ postalCode: '80487',
56
+ }],
57
+ externalId: '00u1e8mutl6wlH3lL4x7',
58
+ displayName: 'Blair Lunceford',
59
+ "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
60
+ manager: {
61
+ value: '2',
62
+ displayName: 'Kathleen Chung',
63
+ },
64
+ division: 'Engineering',
65
+ department: 'Customer Success',
66
+ },
67
+ },
68
+ }
69
+ end
70
+
71
+ context 'with the correct payload, sig_header, and secret' do
72
+ it 'returns a webhook event' do
73
+ webhook = described_class.construct_event(
74
+ payload: @payload,
75
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
76
+ secret: @secret,
77
+ )
78
+
79
+ expect(webhook.data).to eq(@expectation)
80
+ expect(webhook.event).to eq('dsync.user.created')
81
+ expect(webhook.id).to eq('wh_123')
82
+ end
83
+ end
84
+
85
+ context 'with the correct payload, sig_header, secret, and tolerance' do
86
+ it 'returns a webhook event' do
87
+ webhook = described_class.construct_event(
88
+ payload: @payload,
89
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
90
+ secret: @secret,
91
+ tolerance: 300,
92
+ )
93
+
94
+ expect(webhook.data).to eq(@expectation)
95
+ expect(webhook.event).to eq('dsync.user.created')
96
+ expect(webhook.id).to eq('wh_123')
97
+ end
98
+ end
99
+
100
+ context 'with an empty header' do
101
+ it 'raises an error' do
102
+ expect do
103
+ described_class.construct_event(
104
+ payload: @payload,
105
+ sig_header: '',
106
+ secret: @secret,
107
+ )
108
+ end.to raise_error(
109
+ WorkOS::SignatureVerificationError,
110
+ 'Unable to extract timestamp and signature hash from header',
111
+ )
112
+ end
113
+ end
114
+
115
+ context 'with an empty signature hash' do
116
+ it 'raises an error' do
117
+ expect do
118
+ described_class.construct_event(
119
+ payload: @payload,
120
+ sig_header: "t=#{@timestamp.to_i}, v1=",
121
+ secret: @secret,
122
+ )
123
+ end.to raise_error(
124
+ WorkOS::SignatureVerificationError,
125
+ 'No signature hash found with expected scheme v1',
126
+ )
127
+ end
128
+ end
129
+
130
+ context 'with an incorrect signature hash' do
131
+ it 'raises an error' do
132
+ expect do
133
+ described_class.construct_event(
134
+ payload: @payload,
135
+ sig_header: "t=#{@timestamp.to_i}, v1=99999",
136
+ secret: @secret,
137
+ )
138
+ end.to raise_error(
139
+ WorkOS::SignatureVerificationError,
140
+ 'Signature hash does not match the expected signature hash for payload',
141
+ )
142
+ end
143
+ end
144
+
145
+ context 'with an incorrect payload' do
146
+ it 'raises an error' do
147
+ expect do
148
+ described_class.construct_event(
149
+ payload: 'invalid',
150
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
151
+ secret: @secret,
152
+ )
153
+ end.to raise_error(
154
+ WorkOS::SignatureVerificationError,
155
+ 'Signature hash does not match the expected signature hash for payload',
156
+ )
157
+ end
158
+ end
159
+
160
+ context 'with an incorrect webhook secret' do
161
+ it 'raises an error' do
162
+ expect do
163
+ described_class.construct_event(
164
+ payload: @payload,
165
+ sig_header: "t=#{@timestamp.to_i}, v1=#{@signature_hash}",
166
+ secret: 'invalid',
167
+ )
168
+ end.to raise_error(
169
+ WorkOS::SignatureVerificationError,
170
+ 'Signature hash does not match the expected signature hash for payload',
171
+ )
172
+ end
173
+ end
174
+
175
+ context 'with a timestamp outside tolerance' do
176
+ it 'raises an error' do
177
+ expect do
178
+ described_class.construct_event(
179
+ payload: @payload,
180
+ sig_header: "t=9999, v1=#{@signature_hash}",
181
+ secret: @secret,
182
+ )
183
+ end.to raise_error(
184
+ WorkOS::SignatureVerificationError,
185
+ 'Timestamp outside the tolerance zone',
186
+ )
187
+ end
188
+ end
189
+ end
190
+ end
data/spec/spec_helper.rb CHANGED
@@ -18,6 +18,9 @@ require 'webmock/rspec'
18
18
  require 'workos'
19
19
  require 'vcr'
20
20
 
21
+ # Support
22
+ Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
23
+
21
24
  SPEC_ROOT = File.dirname __FILE__
22
25
 
23
26
  VCR.configure do |config|
@@ -2,7 +2,7 @@
2
2
  http_interactions:
3
3
  - request:
4
4
  method: get
5
- uri: https://api.workos.com/directory_users/directory_usr_01E64QS50EAY48S0XJ1AA4WX4D
5
+ uri: https://api.workos.com/directory_users/directory_user_01FAZYNPC8M0HRYTKFP2GNX852
6
6
  body:
7
7
  encoding: US-ASCII
8
8
  string: ''
@@ -14,7 +14,7 @@ http_interactions:
14
14
  Accept:
15
15
  - "*/*"
16
16
  User-Agent:
17
- - WorkOS; ruby/2.7.1; x86_64-darwin19; v0.2.3
17
+ - WorkOS; ruby/3.0.1; x86_64-darwin19; v1.4.0
18
18
  Authorization:
19
19
  - Bearer <API_KEY>
20
20
  response:
@@ -22,16 +22,26 @@ http_interactions:
22
22
  code: 200
23
23
  message: OK
24
24
  headers:
25
- Server:
26
- - Cowboy
25
+ Date:
26
+ - Mon, 09 Aug 2021 17:36:13 GMT
27
+ Content-Type:
28
+ - application/json; charset=utf-8
29
+ Transfer-Encoding:
30
+ - chunked
27
31
  Connection:
28
32
  - keep-alive
29
33
  Vary:
30
34
  - Origin, Accept-Encoding
31
35
  Access-Control-Allow-Credentials:
32
36
  - 'true'
37
+ Content-Security-Policy:
38
+ - 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
39
+ https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
40
+ ''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
33
41
  X-Dns-Prefetch-Control:
34
42
  - 'off'
43
+ Expect-Ct:
44
+ - max-age=0
35
45
  X-Frame-Options:
36
46
  - SAMEORIGIN
37
47
  Strict-Transport-Security:
@@ -40,23 +50,37 @@ http_interactions:
40
50
  - noopen
41
51
  X-Content-Type-Options:
42
52
  - nosniff
53
+ X-Permitted-Cross-Domain-Policies:
54
+ - none
55
+ Referrer-Policy:
56
+ - no-referrer
43
57
  X-Xss-Protection:
44
- - 1; mode=block
58
+ - '0'
45
59
  X-Request-Id:
46
- - fe3f582a-b944-4f2c-a225-ae3a7f099fa0
47
- Content-Type:
48
- - application/json; charset=utf-8
49
- Content-Length:
50
- - '368'
60
+ - 59862449-73e5-4dfd-93ab-3764b1917801
51
61
  Etag:
52
- - W/"170-JjGTDHa7GqXeAwXcua+s3+2z/oA"
53
- Date:
54
- - Thu, 30 Apr 2020 04:43:14 GMT
62
+ - W/"4c3-Ikxt2N0fUuSxCjv+RdYvW8W9Xwo"
55
63
  Via:
56
64
  - 1.1 vegur
65
+ Cf-Cache-Status:
66
+ - DYNAMIC
67
+ Report-To:
68
+ - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=m6%2FJ3BJ75VMwSOtfDQXjt%2FoL29FI%2Bv5VswhZzg6LVkOQi7nyI19Sks%2FkDGCDrSQ%2FMtyU6DI4OFWR9RB1I04IGdhehsY2oPGugIj%2BhHMiJdQEcE6vPAsuaF1HyVnXGvMgRYdurEW1Jr7rSYBWeA%3D%3D"}],"group":"cf-nel","max_age":604800}'
69
+ Nel:
70
+ - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
71
+ Server:
72
+ - cloudflare
73
+ Cf-Ray:
74
+ - 67c2bed0ebe6c7e6-DFW
75
+ Alt-Svc:
76
+ - h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443";
77
+ ma=86400
57
78
  body:
58
- encoding: UTF-8
59
- string: '{"id":"directory_usr_01E64QS50EAY48S0XJ1AA4WX4D","raw_attributes":{"name":{"givenName":"Mark","familyName":"Tran"},"emails":[{"value":"mark@foo-corp.com","primary":true}],"userName":"mark@foo-corp.com","externalId":"118325297729072421906"},"first_name":"Mark","emails":[{"value":"mark@foo-corp.com","primary":true}],"username":"mark@foo-corp.com","last_name":"Tran"}'
79
+ encoding: ASCII-8BIT
80
+ string: '{"object":"directory_user","id":"directory_user_01FAZYNPC8M0HRYTKFP2GNX852","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","idp_id":"6092c280a3f1e19ef6d8cef8","username":"logan@workos.com","emails":[{"primary":true,"value":"logan@workos.com"}],"first_name":"Logan","last_name":"Gingerich","state":"active","raw_attributes":{"id":"6092c280a3f1e19ef6d8cef8","name":"Logan
81
+ Paul Gingerich","teams":["5f696c8e9a63a60e965aaca8"],"spokeId":null,"lastName":"Gingerich","createdAt":"2021-05-05T16:06:24+0000","firstName":"Logan","updatedAt":"2021-07-19T19:17:52+0000","workEmail":"logan@workos.com","department":"Infra","departmentId":"5f27ada9a5e9bc0001a0ae4a"},"custom_attributes":{"department":"Infra"},"groups":[{"object":"directory_group","id":"directory_group_01FAZYNN1NZWMBRAXXDSTB5NFH","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","name":"Department
82
+ - Infra","raw_attributes":{"id":"5f27ada9a5e9bc0001a0ae4a","name":"Infra","parent":"5f27ada9a5e9bc0001a0ae48"}},{"object":"directory_group","id":"directory_group_01FAZYNNQ4HQ6EBF29MPTH7VKB","directory_id":"directory_01FAZYMST676QMTFN1DDJZZX87","name":"Team
83
+ - Platform","raw_attributes":{"id":"5f696c8e9a63a60e965aaca8","name":"Platform","parent":null}}]}'
60
84
  http_version:
61
- recorded_at: Thu, 30 Apr 2020 04:43:15 GMT
85
+ recorded_at: Mon, 09 Aug 2021 17:36:13 GMT
62
86
  recorded_with: VCR 5.0.0
@@ -67,9 +67,9 @@ http_interactions:
67
67
  body:
68
68
  encoding: UTF-8
69
69
  string: '{"object":"list","listMetadata":{"before":null,"after":null},"data":[{"object":"directory","id":"directory_01F7796W20KW0CXEQQEYENT0ZC","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Bamboo
70
- Test","external_key":"rPzV4pdpbaUiKsc6","type":"bamboohr","state":"unlinked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5ZY7XVQZ3DRYEZTH1EPA8BS","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Foo
71
- Corp","external_key":"qV4eyK99QGUaYYa0","type":"okta scim v2.0","state":"linked","domain":"foo-corp.com"},{"object":"directory","id":"directory_01F5XHH1QHX6C2F0Z6WG9YPGCJ","organization_id":"org_01F29YJ068E52HGEB8ZQGC9MJG","name":"Example
72
- Azure SCIM","external_key":"YDKJvbWHKKg66cSk","type":"azure scim v2.0","state":"linked","domain":"example.com"}]}'
70
+ Test","external_key":"rPzV4pdpbaUiKsc6","type":"bamboohr","state":"unlinked","domain":"foo-corp.com", "created_at":"2021-07-02T19:15:39.556Z","updated_at":"2021-07-02T19:31:28.499Z"},{"object":"directory","id":"directory_01F5ZY7XVQZ3DRYEZTH1EPA8BS","organization_id":"org_01EZDF20TZEJXKPSX2BJRN6TV6","name":"Foo
71
+ Corp","external_key":"qV4eyK99QGUaYYa0","type":"okta scim v2.0","state":"linked","domain":"foo-corp.com", "created_at":"2021-07-02T19:15:39.556Z","updated_at":"2021-07-02T19:31:28.499Z"},{"object":"directory","id":"directory_01F5XHH1QHX6C2F0Z6WG9YPGCJ","organization_id":"org_01F29YJ068E52HGEB8ZQGC9MJG","name":"Example
72
+ Azure SCIM","external_key":"YDKJvbWHKKg66cSk","type":"azure scim v2.0","state":"linked","domain":"example.com", "created_at":"2021-07-02T19:15:39.556Z","updated_at":"2021-07-02T19:31:28.499Z"}]}'
73
73
  http_version:
74
74
  recorded_at: Mon, 07 Jun 2021 17:55:30 GMT
75
75
  recorded_with: VCR 5.0.0