workos 2.0.0 → 2.2.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.
- checksums.yaml +4 -4
- data/.semaphore/semaphore.yml +13 -39
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/lib/workos/challenge.rb +54 -0
- data/lib/workos/client.rb +2 -0
- data/lib/workos/directory_sync.rb +1 -0
- data/lib/workos/errors.rb +3 -1
- data/lib/workos/factor.rb +58 -0
- data/lib/workos/mfa.rb +165 -0
- data/lib/workos/sso.rb +38 -15
- data/lib/workos/types/challenge_struct.rb +18 -0
- data/lib/workos/types/factor_struct.rb +19 -0
- data/lib/workos/types/verify_factor_struct.rb +15 -0
- data/lib/workos/types.rb +3 -0
- data/lib/workos/verify_factor.rb +39 -0
- data/lib/workos/version.rb +1 -1
- data/lib/workos/webhooks.rb +9 -7
- data/lib/workos.rb +5 -0
- data/spec/lib/workos/mfa_spec.rb +232 -0
- data/spec/lib/workos/sso_spec.rb +140 -4
- data/spec/lib/workos/webhooks_spec.rb +1 -1
- data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_generic_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_sms_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/challenge_factor_totp_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/delete_factor.yml +80 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_generic_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_sms_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/enroll_factor_totp_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/get_factor_invalid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/get_factor_valid.yml +82 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_expired.yml +84 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_invalid.yml +84 -0
- data/spec/support/fixtures/vcr_cassettes/mfa/verify_factor_generic_valid.yml +82 -0
- metadata +36 -3
data/lib/workos/webhooks.rb
CHANGED
|
@@ -65,7 +65,7 @@ module WorkOS
|
|
|
65
65
|
tolerance: Integer,
|
|
66
66
|
).returns(T::Boolean)
|
|
67
67
|
end
|
|
68
|
-
# rubocop:disable Metrics/MethodLength
|
|
68
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
69
69
|
def verify_header(
|
|
70
70
|
payload:,
|
|
71
71
|
sig_header:,
|
|
@@ -86,7 +86,9 @@ module WorkOS
|
|
|
86
86
|
)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
timestamp_to_time = Time.at(timestamp.to_i / 1000)
|
|
90
|
+
|
|
91
|
+
if timestamp_to_time < Time.now - tolerance
|
|
90
92
|
raise WorkOS::SignatureVerificationError.new(
|
|
91
93
|
message: 'Timestamp outside the tolerance zone',
|
|
92
94
|
)
|
|
@@ -101,12 +103,12 @@ module WorkOS
|
|
|
101
103
|
|
|
102
104
|
true
|
|
103
105
|
end
|
|
104
|
-
# rubocop:enable Metrics/MethodLength
|
|
106
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
105
107
|
|
|
106
108
|
sig do
|
|
107
109
|
params(
|
|
108
110
|
sig_header: String,
|
|
109
|
-
).returns(
|
|
111
|
+
).returns([String, String])
|
|
110
112
|
end
|
|
111
113
|
def get_timestamp_and_signature_hash(
|
|
112
114
|
sig_header:
|
|
@@ -122,12 +124,12 @@ module WorkOS
|
|
|
122
124
|
timestamp = timestamp.sub('t=', '')
|
|
123
125
|
signature_hash = signature_hash.sub('v1=', '')
|
|
124
126
|
|
|
125
|
-
[
|
|
127
|
+
[timestamp, signature_hash]
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
sig do
|
|
129
131
|
params(
|
|
130
|
-
timestamp:
|
|
132
|
+
timestamp: String,
|
|
131
133
|
payload: String,
|
|
132
134
|
secret: String,
|
|
133
135
|
).returns(String)
|
|
@@ -137,7 +139,7 @@ module WorkOS
|
|
|
137
139
|
payload:,
|
|
138
140
|
secret:
|
|
139
141
|
)
|
|
140
|
-
unhashed_string = "#{timestamp
|
|
142
|
+
unhashed_string = "#{timestamp}.#{payload}"
|
|
141
143
|
digest = OpenSSL::Digest.new('sha256')
|
|
142
144
|
OpenSSL::HMAC.hexdigest(digest, secret, unhashed_string)
|
|
143
145
|
end
|
data/lib/workos.rb
CHANGED
|
@@ -44,6 +44,11 @@ module WorkOS
|
|
|
44
44
|
autoload :DirectoryUser, 'workos/directory_user'
|
|
45
45
|
autoload :Webhook, 'workos/webhook'
|
|
46
46
|
autoload :Webhooks, 'workos/webhooks'
|
|
47
|
+
autoload :MFA, 'workos/mfa'
|
|
48
|
+
autoload :Factor, 'workos/factor'
|
|
49
|
+
autoload :Challenge, 'workos/challenge'
|
|
50
|
+
autoload :VerifyFactor, 'workos/verify_factor'
|
|
51
|
+
|
|
47
52
|
|
|
48
53
|
# Errors
|
|
49
54
|
autoload :APIError, 'workos/errors'
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: false
|
|
3
|
+
|
|
4
|
+
describe WorkOS::MFA do
|
|
5
|
+
it_behaves_like 'client'
|
|
6
|
+
describe 'enroll_factor valid requests' do
|
|
7
|
+
context 'enroll factor using valid generic argument' do
|
|
8
|
+
it 'returns a valid factor object' do
|
|
9
|
+
VCR.use_cassette 'mfa/enroll_factor_generic_valid' do
|
|
10
|
+
factor = described_class.enroll_factor(
|
|
11
|
+
type: 'generic_otp',
|
|
12
|
+
)
|
|
13
|
+
expect(factor.type == 'generic_otp')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
context 'enroll factor using valid totp arguments' do
|
|
18
|
+
it 'returns a valid factor object' do
|
|
19
|
+
VCR.use_cassette 'mfa/enroll_factor_totp_valid' do
|
|
20
|
+
factor = described_class.enroll_factor(
|
|
21
|
+
type: 'totp',
|
|
22
|
+
totp_issuer: 'WorkOS',
|
|
23
|
+
totp_user: 'some_user',
|
|
24
|
+
)
|
|
25
|
+
expect(factor.totp.instance_of?(Hash))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
context 'enroll factor using valid sms arguments' do
|
|
30
|
+
it 'returns a valid factor object' do
|
|
31
|
+
VCR.use_cassette 'mfa/enroll_factor_sms_valid' do
|
|
32
|
+
factor = described_class.enroll_factor(
|
|
33
|
+
type: 'sms',
|
|
34
|
+
phone_number: '55555555555',
|
|
35
|
+
)
|
|
36
|
+
expect(factor.sms.instance_of?(Hash))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
describe 'enroll_factor invalid responses' do
|
|
42
|
+
context 'enroll factor throws error if type is not sms or totp' do
|
|
43
|
+
it 'returns an error' do
|
|
44
|
+
expect do
|
|
45
|
+
described_class.enroll_factor(
|
|
46
|
+
type: 'invalid',
|
|
47
|
+
phone_number: '+15005550006',
|
|
48
|
+
)
|
|
49
|
+
end.to raise_error(
|
|
50
|
+
ArgumentError,
|
|
51
|
+
"Type argument must be either 'sms' or 'totp'",
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
context 'enroll factor throws error if type is not sms or totp' do
|
|
56
|
+
it 'returns an error' do
|
|
57
|
+
expect do
|
|
58
|
+
described_class.enroll_factor(
|
|
59
|
+
type: 'totp',
|
|
60
|
+
totp_issuer: 'WorkOS',
|
|
61
|
+
)
|
|
62
|
+
end.to raise_error(
|
|
63
|
+
ArgumentError,
|
|
64
|
+
'Incomplete arguments. Need to specify both totp_issuer and totp_user when type is totp',
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
context 'enroll factor throws error if type sms and phone number is nil' do
|
|
69
|
+
it 'returns an error' do
|
|
70
|
+
expect do
|
|
71
|
+
described_class.enroll_factor(
|
|
72
|
+
type: 'sms',
|
|
73
|
+
)
|
|
74
|
+
end.to raise_error(
|
|
75
|
+
ArgumentError,
|
|
76
|
+
'Incomplete arguments. Need to specify phone_number when type is sms',
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
describe 'challenge factor with valid request arguments' do
|
|
82
|
+
context 'challenge with totp' do
|
|
83
|
+
it 'returns challenge factor object for totp' do
|
|
84
|
+
VCR.use_cassette 'mfa/challenge_factor_totp_valid' do
|
|
85
|
+
challenge_factor = described_class.challenge_factor(
|
|
86
|
+
authentication_factor_id: 'auth_factor_01FZ4TS0MWPZR7GATS7KCXANQZ',
|
|
87
|
+
)
|
|
88
|
+
expect(challenge_factor.authentication_factor_id.class.instance_of?(String))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
context 'challenge with sms' do
|
|
93
|
+
it 'returns a challenge factor object for sms' do
|
|
94
|
+
VCR.use_cassette 'mfa/challenge_factor_sms_valid' do
|
|
95
|
+
challenge_factor = described_class.challenge_factor(
|
|
96
|
+
authentication_factor_id: 'auth_factor_01FZ4TS14D1PHFNZ9GF6YD8M1F',
|
|
97
|
+
sms_template: 'Your code is {{code}}',
|
|
98
|
+
)
|
|
99
|
+
expect(challenge_factor.authentication_factor_id.instance_of?(String))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
context 'challenge with generic' do
|
|
104
|
+
it 'returns a valid challenge factor object for generic otp' do
|
|
105
|
+
VCR.use_cassette 'mfa/challenge_factor_generic_valid' do
|
|
106
|
+
challenge_factor = described_class.challenge_factor(
|
|
107
|
+
authentication_factor_id: 'auth_factor_01FZ4WMXXA09XF6NK1XMKNWB3M',
|
|
108
|
+
)
|
|
109
|
+
expect(challenge_factor.code.instance_of?(String))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
describe 'challenge factor with invalid arguments' do
|
|
115
|
+
context 'challenge with totp mssing authentication_factor_id' do
|
|
116
|
+
it 'returns argument error' do
|
|
117
|
+
expect do
|
|
118
|
+
described_class.challenge_factor
|
|
119
|
+
end.to raise_error(
|
|
120
|
+
ArgumentError,
|
|
121
|
+
"Incomplete arguments: 'authentication_factor_id' is a required argument",
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
describe 'challenge factor with valid requests' do
|
|
127
|
+
context 'verify generic otp' do
|
|
128
|
+
it 'returns a true boolean if the challenge has not been verifed yet' do
|
|
129
|
+
VCR.use_cassette 'mfa/verify_factor_generic_valid' do
|
|
130
|
+
verify_factor = described_class.verify_factor(
|
|
131
|
+
authentication_challenge_id: 'auth_challenge_01FZ4YVRBMXP5ZM0A7BP4AJ12J',
|
|
132
|
+
code: '897792',
|
|
133
|
+
)
|
|
134
|
+
expect(verify_factor.valid == 'true')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
context 'verify generic otp' do
|
|
139
|
+
it 'returns error that the challenge has already been verfied' do
|
|
140
|
+
VCR.use_cassette 'mfa/verify_factor_generic_invalid' do
|
|
141
|
+
expect do
|
|
142
|
+
described_class.verify_factor(
|
|
143
|
+
authentication_challenge_id: 'auth_challenge_01FZ4YVRBMXP5ZM0A7BP4AJ12J',
|
|
144
|
+
code: '897792',
|
|
145
|
+
)
|
|
146
|
+
end.to raise_error(WorkOS::InvalidRequestError)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
context 'verify generic otp' do
|
|
150
|
+
it 'returns error that the challenge has expired' do
|
|
151
|
+
VCR.use_cassette 'mfa/verify_factor_generic_expired' do
|
|
152
|
+
expect do
|
|
153
|
+
described_class.verify_factor(
|
|
154
|
+
authentication_challenge_id: 'auth_challenge_01FZ4YVRBMXP5ZM0A7BP4AJ12J',
|
|
155
|
+
code: '897792',
|
|
156
|
+
)
|
|
157
|
+
end.to raise_error(WorkOS::InvalidRequestError)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
describe 'verify_factor with invalid argument' do
|
|
164
|
+
context 'missing code argument' do
|
|
165
|
+
it 'returns argument error' do
|
|
166
|
+
expect do
|
|
167
|
+
described_class.verify_factor(
|
|
168
|
+
authentication_challenge_id: 'auth_challenge_01FZ4YVRBMXP5ZM0A7BP4AJ12J',
|
|
169
|
+
)
|
|
170
|
+
end.to raise_error(
|
|
171
|
+
ArgumentError,
|
|
172
|
+
"Incomplete arguments: 'authentication_challenge_id' and 'code' are required arguments",
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
context 'missing authentication_challenge_id argument' do
|
|
177
|
+
it '' do
|
|
178
|
+
expect do
|
|
179
|
+
described_class.verify_factor(
|
|
180
|
+
code: '897792',
|
|
181
|
+
)
|
|
182
|
+
end.to raise_error(
|
|
183
|
+
ArgumentError,
|
|
184
|
+
"Incomplete arguments: 'authentication_challenge_id' and 'code' are required arguments",
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
context 'missing code and authentication_challenge_id arguments' do
|
|
189
|
+
it 'returns argument error' do
|
|
190
|
+
expect do
|
|
191
|
+
described_class.verify_factor
|
|
192
|
+
end.to raise_error(
|
|
193
|
+
ArgumentError,
|
|
194
|
+
"Incomplete arguments: 'authentication_challenge_id' and 'code' are required arguments",
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
describe 'tests returning and deleting a factor' do
|
|
200
|
+
context 'returns a factor' do
|
|
201
|
+
it 'uses get_factor to return factor' do
|
|
202
|
+
VCR.use_cassette 'mfa/get_factor_valid' do
|
|
203
|
+
factor = described_class.get_factor(
|
|
204
|
+
id: 'auth_factor_01FZ4WMXXA09XF6NK1XMKNWB3M',
|
|
205
|
+
)
|
|
206
|
+
expect(factor.id.instance_of?(String))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
context 'invalid factor request' do
|
|
211
|
+
it 'uses get_factor and throws error if id is wrong' do
|
|
212
|
+
VCR.use_cassette 'mfa/get_factor_invalid' do
|
|
213
|
+
expect do
|
|
214
|
+
described_class.get_factor(
|
|
215
|
+
id: 'auth_factor_invalid',
|
|
216
|
+
)
|
|
217
|
+
end.to raise_error(WorkOS::APIError)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
context 'deletes facotr' do
|
|
222
|
+
it 'uses delete_factor to delete factor' do
|
|
223
|
+
VCR.use_cassette 'mfa/delete_factor' do
|
|
224
|
+
response = described_class.delete_factor(
|
|
225
|
+
id: 'auth_factor_01FZ4WMXXA09XF6NK1XMKNWB3M',
|
|
226
|
+
)
|
|
227
|
+
expect(response).to be(true)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
data/spec/lib/workos/sso_spec.rb
CHANGED
|
@@ -20,7 +20,6 @@ describe WorkOS::SSO do
|
|
|
20
20
|
end
|
|
21
21
|
it 'returns a valid URL' do
|
|
22
22
|
authorization_url = described_class.authorization_url(**args)
|
|
23
|
-
|
|
24
23
|
expect(URI.parse(authorization_url)).to be_a URI
|
|
25
24
|
end
|
|
26
25
|
|
|
@@ -109,7 +108,145 @@ describe WorkOS::SSO do
|
|
|
109
108
|
end
|
|
110
109
|
end
|
|
111
110
|
|
|
112
|
-
context 'with
|
|
111
|
+
context 'with a domain' do
|
|
112
|
+
let(:args) do
|
|
113
|
+
{
|
|
114
|
+
domain: 'foo.com',
|
|
115
|
+
client_id: 'workos-proj-123',
|
|
116
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
117
|
+
state: {
|
|
118
|
+
next_page: '/dashboard/edit',
|
|
119
|
+
}.to_s,
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
it 'returns a valid URL' do
|
|
123
|
+
authorization_url = described_class.authorization_url(**args)
|
|
124
|
+
|
|
125
|
+
expect(URI.parse(authorization_url)).to be_a URI
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'returns the expected hostname' do
|
|
129
|
+
authorization_url = described_class.authorization_url(**args)
|
|
130
|
+
|
|
131
|
+
expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns the expected query string' do
|
|
135
|
+
authorization_url = described_class.authorization_url(**args)
|
|
136
|
+
|
|
137
|
+
expect(URI.parse(authorization_url).query).to eq(
|
|
138
|
+
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
139
|
+
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
|
|
140
|
+
'edit%22%7D&domain=foo.com',
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'with a domain_hint' do
|
|
146
|
+
let(:args) do
|
|
147
|
+
{
|
|
148
|
+
connection: 'connection_123',
|
|
149
|
+
domain_hint: 'foo.com',
|
|
150
|
+
client_id: 'workos-proj-123',
|
|
151
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
152
|
+
state: {
|
|
153
|
+
next_page: '/dashboard/edit',
|
|
154
|
+
}.to_s,
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
it 'returns a valid URL' do
|
|
158
|
+
authorization_url = described_class.authorization_url(**args)
|
|
159
|
+
|
|
160
|
+
expect(URI.parse(authorization_url)).to be_a URI
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'returns the expected hostname' do
|
|
164
|
+
authorization_url = described_class.authorization_url(**args)
|
|
165
|
+
|
|
166
|
+
expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'returns the expected query string' do
|
|
170
|
+
authorization_url = described_class.authorization_url(**args)
|
|
171
|
+
|
|
172
|
+
expect(URI.parse(authorization_url).query).to eq(
|
|
173
|
+
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
174
|
+
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2' \
|
|
175
|
+
'Fedit%22%7D&domain_hint=foo.com&connection=connection_123',
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
context 'with a login_hint' do
|
|
181
|
+
let(:args) do
|
|
182
|
+
{
|
|
183
|
+
connection: 'connection_123',
|
|
184
|
+
login_hint: 'foo@workos.com',
|
|
185
|
+
client_id: 'workos-proj-123',
|
|
186
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
187
|
+
state: {
|
|
188
|
+
next_page: '/dashboard/edit',
|
|
189
|
+
}.to_s,
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
it 'returns a valid URL' do
|
|
193
|
+
authorization_url = described_class.authorization_url(**args)
|
|
194
|
+
|
|
195
|
+
expect(URI.parse(authorization_url)).to be_a URI
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'returns the expected hostname' do
|
|
199
|
+
authorization_url = described_class.authorization_url(**args)
|
|
200
|
+
|
|
201
|
+
expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'returns the expected query string' do
|
|
205
|
+
authorization_url = described_class.authorization_url(**args)
|
|
206
|
+
|
|
207
|
+
expect(URI.parse(authorization_url).query).to eq(
|
|
208
|
+
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
209
|
+
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2' \
|
|
210
|
+
'Fedit%22%7D&login_hint=foo%40workos.com&connection=connection_123',
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
context 'with an organization' do
|
|
216
|
+
let(:args) do
|
|
217
|
+
{
|
|
218
|
+
organization: 'org_123',
|
|
219
|
+
client_id: 'workos-proj-123',
|
|
220
|
+
redirect_uri: 'foo.com/auth/callback',
|
|
221
|
+
state: {
|
|
222
|
+
next_page: '/dashboard/edit',
|
|
223
|
+
}.to_s,
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
it 'returns a valid URL' do
|
|
227
|
+
authorization_url = described_class.authorization_url(**args)
|
|
228
|
+
|
|
229
|
+
expect(URI.parse(authorization_url)).to be_a URI
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'returns the expected hostname' do
|
|
233
|
+
authorization_url = described_class.authorization_url(**args)
|
|
234
|
+
|
|
235
|
+
expect(URI.parse(authorization_url).host).to eq(WorkOS::API_HOSTNAME)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'returns the expected query string' do
|
|
239
|
+
authorization_url = described_class.authorization_url(**args)
|
|
240
|
+
|
|
241
|
+
expect(URI.parse(authorization_url).query).to eq(
|
|
242
|
+
'client_id=workos-proj-123&redirect_uri=foo.com%2Fauth%2Fcallback' \
|
|
243
|
+
'&response_type=code&state=%7B%3Anext_page%3D%3E%22%2Fdashboard%2F' \
|
|
244
|
+
'edit%22%7D&organization=org_123',
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
context 'with neither connection, domain, provider, or organization' do
|
|
113
250
|
let(:args) do
|
|
114
251
|
{
|
|
115
252
|
client_id: 'workos-proj-123',
|
|
@@ -124,7 +261,7 @@ describe WorkOS::SSO do
|
|
|
124
261
|
described_class.authorization_url(**args)
|
|
125
262
|
end.to raise_error(
|
|
126
263
|
ArgumentError,
|
|
127
|
-
'Either connection, domain, or
|
|
264
|
+
'Either connection, domain, provider, or organization is required.',
|
|
128
265
|
)
|
|
129
266
|
end
|
|
130
267
|
end
|
|
@@ -177,7 +314,6 @@ describe WorkOS::SSO do
|
|
|
177
314
|
verified_email: true,
|
|
178
315
|
},
|
|
179
316
|
}
|
|
180
|
-
|
|
181
317
|
expect(profile.to_json).to eq(expectation)
|
|
182
318
|
end
|
|
183
319
|
end
|
|
@@ -177,7 +177,7 @@ describe WorkOS::Webhooks do
|
|
|
177
177
|
expect do
|
|
178
178
|
described_class.construct_event(
|
|
179
179
|
payload: @payload,
|
|
180
|
-
sig_header: "t
|
|
180
|
+
sig_header: "t=#{@timestamp.to_i - (200 * 1000)}, v1=#{@signature_hash}",
|
|
181
181
|
secret: @secret,
|
|
182
182
|
)
|
|
183
183
|
end.to raise_error(
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
http_interactions:
|
|
3
|
+
- request:
|
|
4
|
+
method: post
|
|
5
|
+
uri: https://api.workos.com/auth/factors/challenge
|
|
6
|
+
body:
|
|
7
|
+
encoding: UTF-8
|
|
8
|
+
string: '{"sms_template":null,"authentication_factor_id":"auth_factor_01FZ4WMXXA09XF6NK1XMKNWB3M"}'
|
|
9
|
+
headers:
|
|
10
|
+
Content-Type:
|
|
11
|
+
- application/json
|
|
12
|
+
Accept-Encoding:
|
|
13
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
|
14
|
+
Accept:
|
|
15
|
+
- "*/*"
|
|
16
|
+
User-Agent:
|
|
17
|
+
- WorkOS; ruby/3.0.2; arm64-darwin21; v2.1.1
|
|
18
|
+
Authorization:
|
|
19
|
+
- Bearer <API_KEY>
|
|
20
|
+
response:
|
|
21
|
+
status:
|
|
22
|
+
code: 201
|
|
23
|
+
message: Created
|
|
24
|
+
headers:
|
|
25
|
+
Date:
|
|
26
|
+
- Sun, 27 Mar 2022 05:15:26 GMT
|
|
27
|
+
Content-Type:
|
|
28
|
+
- application/json; charset=utf-8
|
|
29
|
+
Content-Length:
|
|
30
|
+
- '290'
|
|
31
|
+
Connection:
|
|
32
|
+
- keep-alive
|
|
33
|
+
Content-Security-Policy:
|
|
34
|
+
- 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
|
|
35
|
+
https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
|
|
36
|
+
''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
|
|
37
|
+
X-Dns-Prefetch-Control:
|
|
38
|
+
- 'off'
|
|
39
|
+
Expect-Ct:
|
|
40
|
+
- max-age=0
|
|
41
|
+
X-Frame-Options:
|
|
42
|
+
- SAMEORIGIN
|
|
43
|
+
Strict-Transport-Security:
|
|
44
|
+
- max-age=15552000; includeSubDomains
|
|
45
|
+
X-Download-Options:
|
|
46
|
+
- noopen
|
|
47
|
+
X-Content-Type-Options:
|
|
48
|
+
- nosniff
|
|
49
|
+
X-Permitted-Cross-Domain-Policies:
|
|
50
|
+
- none
|
|
51
|
+
Referrer-Policy:
|
|
52
|
+
- no-referrer
|
|
53
|
+
X-Xss-Protection:
|
|
54
|
+
- '0'
|
|
55
|
+
Vary:
|
|
56
|
+
- Origin, Accept-Encoding
|
|
57
|
+
Access-Control-Allow-Credentials:
|
|
58
|
+
- 'true'
|
|
59
|
+
X-Request-Id:
|
|
60
|
+
- 6f320a1f-92d8-496c-9c30-69355787d21e
|
|
61
|
+
Etag:
|
|
62
|
+
- W/"122-QtSiaXex7UKEyydEC3oPpuHzGNw"
|
|
63
|
+
Via:
|
|
64
|
+
- 1.1 vegur
|
|
65
|
+
Cf-Cache-Status:
|
|
66
|
+
- DYNAMIC
|
|
67
|
+
Report-To:
|
|
68
|
+
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=2kePZ4Q7kGQ1by5iXZxRevmSkQq4vTbW1vXTqFev99eLBrrfEHOLK2%2FE0ItWB2GAKoQvhPBk3rhS%2FKg0rtK3ZH4DGTf%2FEQKGFPT6BxtCqQE2L%2ByfEv1AgU152ZwIBPVYjQ%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
|
+
- 6f25a5f0dddf088d-SEA
|
|
75
|
+
Alt-Svc:
|
|
76
|
+
- h3=":443"; ma=86400, h3-29=":443"; ma=86400
|
|
77
|
+
body:
|
|
78
|
+
encoding: UTF-8
|
|
79
|
+
string: '{"object":"authentication_challenge","id":"auth_challenge_01FZ4WSWV2SCEDX3GKY1NA9YTN","created_at":"2022-03-27T05:15:26.432Z","updated_at":"2022-03-27T05:15:26.432Z","expires_at":"2022-03-27T05:25:26.434Z","code":"541295","authentication_factor_id":"auth_factor_01FZ4WMXXA09XF6NK1XMKNWB3M"}'
|
|
80
|
+
http_version:
|
|
81
|
+
recorded_at: Sun, 27 Mar 2022 05:15:26 GMT
|
|
82
|
+
recorded_with: VCR 5.0.0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
http_interactions:
|
|
3
|
+
- request:
|
|
4
|
+
method: post
|
|
5
|
+
uri: https://api.workos.com/auth/factors/challenge
|
|
6
|
+
body:
|
|
7
|
+
encoding: UTF-8
|
|
8
|
+
string: '{"sms_template":"Your code is {{code}}","authentication_factor_id":"auth_factor_01FZ4TS14D1PHFNZ9GF6YD8M1F"}'
|
|
9
|
+
headers:
|
|
10
|
+
Content-Type:
|
|
11
|
+
- application/json
|
|
12
|
+
Accept-Encoding:
|
|
13
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
|
14
|
+
Accept:
|
|
15
|
+
- "*/*"
|
|
16
|
+
User-Agent:
|
|
17
|
+
- WorkOS; ruby/3.0.2; arm64-darwin21; v2.1.1
|
|
18
|
+
Authorization:
|
|
19
|
+
- Bearer <API_KEY>
|
|
20
|
+
response:
|
|
21
|
+
status:
|
|
22
|
+
code: 201
|
|
23
|
+
message: Created
|
|
24
|
+
headers:
|
|
25
|
+
Date:
|
|
26
|
+
- Sun, 27 Mar 2022 05:03:26 GMT
|
|
27
|
+
Content-Type:
|
|
28
|
+
- application/json; charset=utf-8
|
|
29
|
+
Content-Length:
|
|
30
|
+
- '274'
|
|
31
|
+
Connection:
|
|
32
|
+
- keep-alive
|
|
33
|
+
Content-Security-Policy:
|
|
34
|
+
- 'default-src ''self'';base-uri ''self'';block-all-mixed-content;font-src ''self''
|
|
35
|
+
https: data:;frame-ancestors ''self'';img-src ''self'' data:;object-src ''none'';script-src
|
|
36
|
+
''self'';script-src-attr ''none'';style-src ''self'' https: ''unsafe-inline'';upgrade-insecure-requests'
|
|
37
|
+
X-Dns-Prefetch-Control:
|
|
38
|
+
- 'off'
|
|
39
|
+
Expect-Ct:
|
|
40
|
+
- max-age=0
|
|
41
|
+
X-Frame-Options:
|
|
42
|
+
- SAMEORIGIN
|
|
43
|
+
Strict-Transport-Security:
|
|
44
|
+
- max-age=15552000; includeSubDomains
|
|
45
|
+
X-Download-Options:
|
|
46
|
+
- noopen
|
|
47
|
+
X-Content-Type-Options:
|
|
48
|
+
- nosniff
|
|
49
|
+
X-Permitted-Cross-Domain-Policies:
|
|
50
|
+
- none
|
|
51
|
+
Referrer-Policy:
|
|
52
|
+
- no-referrer
|
|
53
|
+
X-Xss-Protection:
|
|
54
|
+
- '0'
|
|
55
|
+
Vary:
|
|
56
|
+
- Origin, Accept-Encoding
|
|
57
|
+
Access-Control-Allow-Credentials:
|
|
58
|
+
- 'true'
|
|
59
|
+
X-Request-Id:
|
|
60
|
+
- b9c66c87-adf4-4a9a-854f-d838f966ea5c
|
|
61
|
+
Etag:
|
|
62
|
+
- W/"112-VpD9nscbxE6VOcsJlK2RHEzlq3s"
|
|
63
|
+
Via:
|
|
64
|
+
- 1.1 vegur
|
|
65
|
+
Cf-Cache-Status:
|
|
66
|
+
- DYNAMIC
|
|
67
|
+
Report-To:
|
|
68
|
+
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=yNaSbl1oXtixmxb7lJOn7tDKzD0mN8jSFHJmTsZfD6YTlSGeMrdBgfi3LUFeDv1ldKcdNe5eZ%2BkRrVM986RUizGOhL2xzdl2AkJEdudIRaaJCMdWbjQDmGLbC4OPFajMIA%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
|
+
- 6f25945b9ee639b4-SEA
|
|
75
|
+
Alt-Svc:
|
|
76
|
+
- h3=":443"; ma=86400, h3-29=":443"; ma=86400
|
|
77
|
+
body:
|
|
78
|
+
encoding: UTF-8
|
|
79
|
+
string: '{"object":"authentication_challenge","id":"auth_challenge_01FZ4W3XG1VD8ZD5TXSYQMFMSR","created_at":"2022-03-27T05:03:26.203Z","updated_at":"2022-03-27T05:03:26.203Z","expires_at":"2022-03-27T05:13:26.204Z","authentication_factor_id":"auth_factor_01FZ4TS14D1PHFNZ9GF6YD8M1F"}'
|
|
80
|
+
http_version:
|
|
81
|
+
recorded_at: Sun, 27 Mar 2022 05:03:26 GMT
|
|
82
|
+
recorded_with: VCR 5.0.0
|