workos 1.3.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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