stytch 10.25.0 → 10.26.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/lib/stytch/b2b_client.rb +10 -2
- data/lib/stytch/b2b_discovery.rb +5 -5
- data/lib/stytch/b2b_idp.rb +266 -0
- data/lib/stytch/b2b_organizations.rb +14 -12
- data/lib/stytch/b2b_otp.rb +4 -2
- data/lib/stytch/b2b_passwords.rb +3 -0
- data/lib/stytch/b2b_rbac.rb +2 -2
- data/lib/stytch/b2b_sessions.rb +3 -3
- data/lib/stytch/b2b_totps.rb +3 -1
- data/lib/stytch/client.rb +16 -2
- data/lib/stytch/fraud.rb +6 -3
- data/lib/stytch/idp.rb +251 -0
- data/lib/stytch/otps.rb +2 -2
- data/lib/stytch/passwords.rb +1 -1
- data/lib/stytch/rbac.rb +49 -0
- data/lib/stytch/rbac_local.rb +66 -3
- data/lib/stytch/sessions.rb +37 -10
- data/lib/stytch/version.rb +1 -1
- data/lib/stytch.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a905e33f9cc2881ad04adabc476f64db1cfc8836b1e012dd6692bd411ff32cd
|
4
|
+
data.tar.gz: bfa6c0c4d4858a8f7154bb85b359bd311b0ea17becf11fb89373bb721fe74d0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: acb3ded234b14fd6fbff16c1e15942a6a7b30bf30e4d3c763b0645e2e61c9bb7aab86f3b8583b1e5aa338bba49b36fd70f80887aed449dfcf854defac6bf0ede
|
7
|
+
data.tar.gz: 00cc8b6ad8d878561322055da00b0fed3a2265dbb7171ab46f89594fe0ba43636b8f86cd0c04c0f0b57c3ec89f0114f82667ef46d021697066bf0fb0a66d726b
|
data/lib/stytch/b2b_client.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
# !!!
|
2
|
+
# WARNING: This file is autogenerated
|
3
|
+
# Only modify code within MANUAL() sections
|
4
|
+
# or your changes may be overwritten later!
|
5
|
+
# !!!
|
6
|
+
|
1
7
|
# frozen_string_literal: true
|
2
8
|
|
3
9
|
require_relative 'b2b_discovery'
|
10
|
+
require_relative 'b2b_idp'
|
4
11
|
require_relative 'b2b_impersonation'
|
5
12
|
require_relative 'b2b_magic_links'
|
6
13
|
require_relative 'b2b_oauth'
|
@@ -23,7 +30,7 @@ module StytchB2B
|
|
23
30
|
class Client
|
24
31
|
ENVIRONMENTS = %i[live test].freeze
|
25
32
|
|
26
|
-
attr_reader :connected_app, :discovery, :fraud, :impersonation, :m2m, :magic_links, :oauth, :otps, :organizations, :passwords, :project, :rbac, :recovery_codes, :scim, :sso, :sessions, :totps
|
33
|
+
attr_reader :connected_app, :discovery, :fraud, :impersonation, :m2m, :magic_links, :oauth, :otps, :organizations, :passwords, :project, :rbac, :recovery_codes, :scim, :sso, :sessions, :totps, :idp
|
27
34
|
|
28
35
|
def initialize(project_id:, secret:, env: nil, fraud_env: nil, &block)
|
29
36
|
@api_host = api_host(env, project_id)
|
@@ -35,7 +42,8 @@ module StytchB2B
|
|
35
42
|
create_connection(&block)
|
36
43
|
|
37
44
|
rbac = StytchB2B::RBAC.new(@connection)
|
38
|
-
@policy_cache =
|
45
|
+
@policy_cache = Stytch::PolicyCache.new(rbac_client: rbac)
|
46
|
+
@idp = StytchB2B::IDP.new(@connection, @project_id, @policy_cache)
|
39
47
|
|
40
48
|
@connected_app = Stytch::ConnectedApp.new(@connection)
|
41
49
|
@discovery = StytchB2B::Discovery.new(@connection)
|
data/lib/stytch/b2b_discovery.rb
CHANGED
@@ -197,7 +197,7 @@ module StytchB2B
|
|
197
197
|
# sso_jit_provisioning::
|
198
198
|
# The authentication setting that controls the JIT provisioning of Members when authenticating via SSO. The accepted values are:
|
199
199
|
#
|
200
|
-
# `ALL_ALLOWED` – new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
200
|
+
# `ALL_ALLOWED` – the default setting, new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
201
201
|
#
|
202
202
|
# `RESTRICTED` – only new Members with SSO logins that comply with `sso_jit_provisioning_allowed_connections` can be provisioned upon authentication.
|
203
203
|
#
|
@@ -215,7 +215,7 @@ module StytchB2B
|
|
215
215
|
#
|
216
216
|
# `RESTRICTED` – only new Members with verified emails that comply with `email_allowed_domains` can be provisioned upon authentication via Email Magic Link or OAuth.
|
217
217
|
#
|
218
|
-
# `NOT_ALLOWED` –
|
218
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning via Email Magic Link and OAuth.
|
219
219
|
#
|
220
220
|
# The type of this field is nilable +String+.
|
221
221
|
# email_invites::
|
@@ -273,7 +273,7 @@ module StytchB2B
|
|
273
273
|
#
|
274
274
|
# `RESTRICTED` – only new Members with tenants in `allowed_oauth_tenants` can JIT provision via tenant.
|
275
275
|
#
|
276
|
-
# `NOT_ALLOWED` –
|
276
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning by OAuth Tenant.
|
277
277
|
#
|
278
278
|
# The type of this field is nilable +String+.
|
279
279
|
# allowed_oauth_tenants::
|
@@ -282,7 +282,7 @@ module StytchB2B
|
|
282
282
|
# first_party_connected_apps_allowed_type::
|
283
283
|
# The authentication setting that sets the Organization's policy towards first party Connected Apps. The accepted values are:
|
284
284
|
#
|
285
|
-
# `ALL_ALLOWED` – any first party Connected App in the Project is permitted for use by Members.
|
285
|
+
# `ALL_ALLOWED` – the default setting, any first party Connected App in the Project is permitted for use by Members.
|
286
286
|
#
|
287
287
|
# `RESTRICTED` – only first party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
288
288
|
#
|
@@ -295,7 +295,7 @@ module StytchB2B
|
|
295
295
|
# third_party_connected_apps_allowed_type::
|
296
296
|
# The authentication setting that sets the Organization's policy towards third party Connected Apps. The accepted values are:
|
297
297
|
#
|
298
|
-
# `ALL_ALLOWED` – any third party Connected App in the Project is permitted for use by Members.
|
298
|
+
# `ALL_ALLOWED` – the default setting, any third party Connected App in the Project is permitted for use by Members.
|
299
299
|
#
|
300
300
|
# `RESTRICTED` – only third party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
301
301
|
#
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
require 'json/jwt'
|
5
|
+
require_relative 'errors'
|
6
|
+
require_relative 'request_helper'
|
7
|
+
require_relative 'rbac_local'
|
8
|
+
|
9
|
+
module StytchB2B
|
10
|
+
class IDP
|
11
|
+
include Stytch::RequestHelper
|
12
|
+
|
13
|
+
def initialize(connection, project_id, policy_cache)
|
14
|
+
@connection = connection
|
15
|
+
@project_id = project_id
|
16
|
+
@policy_cache = policy_cache
|
17
|
+
@non_custom_claim_keys = [
|
18
|
+
'aud',
|
19
|
+
'exp',
|
20
|
+
'iat',
|
21
|
+
'iss',
|
22
|
+
'jti',
|
23
|
+
'nbf',
|
24
|
+
'sub',
|
25
|
+
'active',
|
26
|
+
'client_id',
|
27
|
+
'request_id',
|
28
|
+
'scope',
|
29
|
+
'status_code',
|
30
|
+
'token_type',
|
31
|
+
'https://stytch.com/organization'
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Introspects a token JWT from an authorization code response.
|
36
|
+
# Access tokens are JWTs signed with the project's JWKs. Refresh tokens are opaque tokens.
|
37
|
+
# Access tokens contain a standard set of claims as well as any custom claims generated from templates.
|
38
|
+
#
|
39
|
+
# == Parameters:
|
40
|
+
# token::
|
41
|
+
# The access token (or refresh token) to introspect.
|
42
|
+
# The type of this field is +String+.
|
43
|
+
# client_id::
|
44
|
+
# The ID of the client.
|
45
|
+
# The type of this field is +String+.
|
46
|
+
# client_secret::
|
47
|
+
# The secret of the client.
|
48
|
+
# The type of this field is nilable +String+.
|
49
|
+
# token_type_hint::
|
50
|
+
# A hint on what the token contains. Valid fields are 'access_token' and 'refresh_token'.
|
51
|
+
# The type of this field is +String+.
|
52
|
+
# authorization_check::
|
53
|
+
# Optional authorization check object.
|
54
|
+
# The type of this field is nilable +Hash+.
|
55
|
+
#
|
56
|
+
# == Returns:
|
57
|
+
# An object with the following fields:
|
58
|
+
# subject::
|
59
|
+
# The subject of the token.
|
60
|
+
# The type of this field is +String+.
|
61
|
+
# scope::
|
62
|
+
# The scope of the token.
|
63
|
+
# The type of this field is +String+.
|
64
|
+
# audience::
|
65
|
+
# The audience of the token.
|
66
|
+
# The type of this field is +String+.
|
67
|
+
# expires_at::
|
68
|
+
# The expiration time of the token.
|
69
|
+
# The type of this field is +Integer+.
|
70
|
+
# issued_at::
|
71
|
+
# The issued at time of the token.
|
72
|
+
# The type of this field is +Integer+.
|
73
|
+
# issuer::
|
74
|
+
# The issuer of the token.
|
75
|
+
# The type of this field is +String+.
|
76
|
+
# not_before::
|
77
|
+
# The not before time of the token.
|
78
|
+
# The type of this field is +Integer+.
|
79
|
+
# token_type::
|
80
|
+
# The type of the token.
|
81
|
+
# The type of this field is +String+.
|
82
|
+
# custom_claims::
|
83
|
+
# Custom claims in the token.
|
84
|
+
# The type of this field is +Hash+.
|
85
|
+
# organization_claim::
|
86
|
+
# The organization claim in the token.
|
87
|
+
# The type of this field is +Hash+.
|
88
|
+
def introspect_token_network(
|
89
|
+
token:,
|
90
|
+
client_id:,
|
91
|
+
client_secret: nil,
|
92
|
+
token_type_hint: 'access_token',
|
93
|
+
authorization_check: nil
|
94
|
+
)
|
95
|
+
headers = {}
|
96
|
+
data = {
|
97
|
+
'token' => token,
|
98
|
+
'client_id' => client_id,
|
99
|
+
'token_type_hint' => token_type_hint
|
100
|
+
}
|
101
|
+
data['client_secret'] = client_secret unless client_secret.nil?
|
102
|
+
|
103
|
+
url = @connection.url_prefix + '/v1/oauth2/introspect'
|
104
|
+
jwt_response = post_request(url, data, headers)
|
105
|
+
|
106
|
+
return nil unless jwt_response['active']
|
107
|
+
|
108
|
+
custom_claims = jwt_response.reject { |k, _| @non_custom_claim_keys.include?(k) }
|
109
|
+
organization_claim = jwt_response['https://stytch.com/organization']
|
110
|
+
organization_id = organization_claim['organization_id']
|
111
|
+
scope = jwt_response['scope']
|
112
|
+
|
113
|
+
if authorization_check
|
114
|
+
@policy_cache.perform_authorization_check(
|
115
|
+
subject_roles: scope.split,
|
116
|
+
authorization_check: authorization_check,
|
117
|
+
subject_org_id: organization_id
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
{
|
122
|
+
'subject' => jwt_response['sub'],
|
123
|
+
'scope' => jwt_response['scope'],
|
124
|
+
'audience' => jwt_response['aud'],
|
125
|
+
'expires_at' => jwt_response['exp'],
|
126
|
+
'issued_at' => jwt_response['iat'],
|
127
|
+
'issuer' => jwt_response['iss'],
|
128
|
+
'not_before' => jwt_response['nbf'],
|
129
|
+
'token_type' => jwt_response['token_type'],
|
130
|
+
'custom_claims' => custom_claims,
|
131
|
+
'organization_claim' => organization_claim
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
# Introspects a token JWT from an authorization code response.
|
136
|
+
# Access tokens are JWTs signed with the project's JWKs. Refresh tokens are opaque tokens.
|
137
|
+
# Access tokens contain a standard set of claims as well as any custom claims generated from templates.
|
138
|
+
#
|
139
|
+
# == Parameters:
|
140
|
+
# access_token::
|
141
|
+
# The access token (or refresh token) to introspect.
|
142
|
+
# The type of this field is +String+.
|
143
|
+
# authorization_check::
|
144
|
+
# Optional authorization check object.
|
145
|
+
# The type of this field is nilable +Hash+.
|
146
|
+
#
|
147
|
+
# == Returns:
|
148
|
+
# An object with the following fields:
|
149
|
+
# subject::
|
150
|
+
# The subject of the token.
|
151
|
+
# The type of this field is +String+.
|
152
|
+
# scope::
|
153
|
+
# The scope of the token.
|
154
|
+
# The type of this field is +String+.
|
155
|
+
# audience::
|
156
|
+
# The audience of the token.
|
157
|
+
# The type of this field is +String+.
|
158
|
+
# expires_at::
|
159
|
+
# The expiration time of the token.
|
160
|
+
# The type of this field is +Integer+.
|
161
|
+
# issued_at::
|
162
|
+
# The issued at time of the token.
|
163
|
+
# The type of this field is +Integer+.
|
164
|
+
# issuer::
|
165
|
+
# The issuer of the token.
|
166
|
+
# The type of this field is +String+.
|
167
|
+
# not_before::
|
168
|
+
# The not before time of the token.
|
169
|
+
# The type of this field is +Integer+.
|
170
|
+
# token_type::
|
171
|
+
# The type of the token.
|
172
|
+
# The type of this field is +String+.
|
173
|
+
# custom_claims::
|
174
|
+
# Custom claims in the token.
|
175
|
+
# The type of this field is +Hash+.
|
176
|
+
# organization_claim::
|
177
|
+
# The organization claim in the token.
|
178
|
+
# The type of this field is +Hash+.
|
179
|
+
def introspect_access_token_local(
|
180
|
+
access_token:,
|
181
|
+
authorization_check: nil
|
182
|
+
)
|
183
|
+
scope_claim = 'scope'
|
184
|
+
organization_claim = 'https://stytch.com/organization'
|
185
|
+
|
186
|
+
# Create a JWKS loader similar to other classes in the codebase
|
187
|
+
@cache_last_update = 0
|
188
|
+
jwks_loader = lambda do |options|
|
189
|
+
@cached_keys = nil if options[:invalidate] && @cache_last_update < Time.now.to_i - 300
|
190
|
+
if @cached_keys.nil?
|
191
|
+
@cached_keys = get_jwks(project_id: @project_id)
|
192
|
+
@cache_last_update = Time.now.to_i
|
193
|
+
end
|
194
|
+
@cached_keys
|
195
|
+
end
|
196
|
+
|
197
|
+
begin
|
198
|
+
decoded_jwt = JWT.decode(
|
199
|
+
access_token,
|
200
|
+
nil,
|
201
|
+
true,
|
202
|
+
{
|
203
|
+
algorithms: ['RS256'],
|
204
|
+
jwks: jwks_loader,
|
205
|
+
iss: ["stytch.com/#{@project_id}", @connection.url_prefix],
|
206
|
+
aud: @project_id
|
207
|
+
}
|
208
|
+
)[0]
|
209
|
+
|
210
|
+
generic_claims = decoded_jwt
|
211
|
+
custom_claims = generic_claims.reject { |k, _| @non_custom_claim_keys.include?(k) }
|
212
|
+
organization_claim_data = generic_claims[organization_claim]
|
213
|
+
organization_id = organization_claim_data['organization_id']
|
214
|
+
scope = generic_claims[scope_claim]
|
215
|
+
|
216
|
+
if authorization_check
|
217
|
+
@policy_cache.perform_authorization_check(
|
218
|
+
subject_roles: scope.split,
|
219
|
+
authorization_check: authorization_check,
|
220
|
+
subject_org_id: organization_id
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
{
|
225
|
+
'subject' => generic_claims['sub'],
|
226
|
+
'scope' => generic_claims[scope_claim],
|
227
|
+
'audience' => generic_claims['aud'],
|
228
|
+
'expires_at' => generic_claims['exp'],
|
229
|
+
'issued_at' => generic_claims['iat'],
|
230
|
+
'issuer' => generic_claims['iss'],
|
231
|
+
'not_before' => generic_claims['nbf'],
|
232
|
+
'token_type' => 'access_token',
|
233
|
+
'custom_claims' => custom_claims,
|
234
|
+
'organization_claim' => organization_claim_data
|
235
|
+
}
|
236
|
+
rescue JWT::InvalidIssuerError
|
237
|
+
raise Stytch::JWTInvalidIssuerError
|
238
|
+
rescue JWT::InvalidAudError
|
239
|
+
raise Stytch::JWTInvalidAudienceError
|
240
|
+
rescue JWT::ExpiredSignature
|
241
|
+
raise Stytch::JWTExpiredSignatureError
|
242
|
+
rescue JWT::IncorrectAlgorithm
|
243
|
+
raise Stytch::JWTIncorrectAlgorithmError
|
244
|
+
rescue JWT::DecodeError
|
245
|
+
nil
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Gets the JWKS for the project.
|
250
|
+
#
|
251
|
+
# == Parameters:
|
252
|
+
# project_id::
|
253
|
+
# The ID of the project.
|
254
|
+
# The type of this field is +String+.
|
255
|
+
#
|
256
|
+
# == Returns:
|
257
|
+
# The JWKS for the project.
|
258
|
+
# The type of this field is +Hash+.
|
259
|
+
def get_jwks(project_id:)
|
260
|
+
headers = {}
|
261
|
+
query_params = {}
|
262
|
+
request = request_with_query_params("/v1/b2b/sessions/jwks/#{project_id}", query_params)
|
263
|
+
get_request(request, headers)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -97,7 +97,7 @@ module StytchB2B
|
|
97
97
|
|
98
98
|
# Creates an Organization. An `organization_name` and a unique `organization_slug` are required.
|
99
99
|
#
|
100
|
-
#
|
100
|
+
# If no Organization authentication setting parameters are passed in, `email_invites` will default to `ALL_ALLOWED` so that the Organization has a way to add Members. Otherwise, `email_invites` will default to `NOT_ALLOWED`.
|
101
101
|
#
|
102
102
|
# *See the [Organization authentication settings](https://stytch.com/docs/b2b/api/org-auth-settings) resource to learn more about fields like `email_jit_provisioning`, `email_invites`, `sso_jit_provisioning`, etc., and their behaviors.
|
103
103
|
#
|
@@ -117,7 +117,7 @@ module StytchB2B
|
|
117
117
|
# sso_jit_provisioning::
|
118
118
|
# The authentication setting that controls the JIT provisioning of Members when authenticating via SSO. The accepted values are:
|
119
119
|
#
|
120
|
-
# `ALL_ALLOWED` – new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
120
|
+
# `ALL_ALLOWED` – the default setting, new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
121
121
|
#
|
122
122
|
# `RESTRICTED` – only new Members with SSO logins that comply with `sso_jit_provisioning_allowed_connections` can be provisioned upon authentication.
|
123
123
|
#
|
@@ -135,7 +135,7 @@ module StytchB2B
|
|
135
135
|
#
|
136
136
|
# `RESTRICTED` – only new Members with verified emails that comply with `email_allowed_domains` can be provisioned upon authentication via Email Magic Link or OAuth.
|
137
137
|
#
|
138
|
-
# `NOT_ALLOWED` –
|
138
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning via Email Magic Link and OAuth.
|
139
139
|
#
|
140
140
|
# The type of this field is nilable +String+.
|
141
141
|
# email_invites::
|
@@ -193,7 +193,7 @@ module StytchB2B
|
|
193
193
|
#
|
194
194
|
# `RESTRICTED` – only new Members with tenants in `allowed_oauth_tenants` can JIT provision via tenant.
|
195
195
|
#
|
196
|
-
# `NOT_ALLOWED` –
|
196
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning by OAuth Tenant.
|
197
197
|
#
|
198
198
|
# The type of this field is nilable +String+.
|
199
199
|
# allowed_oauth_tenants::
|
@@ -205,7 +205,7 @@ module StytchB2B
|
|
205
205
|
# first_party_connected_apps_allowed_type::
|
206
206
|
# The authentication setting that sets the Organization's policy towards first party Connected Apps. The accepted values are:
|
207
207
|
#
|
208
|
-
# `ALL_ALLOWED` – any first party Connected App in the Project is permitted for use by Members.
|
208
|
+
# `ALL_ALLOWED` – the default setting, any first party Connected App in the Project is permitted for use by Members.
|
209
209
|
#
|
210
210
|
# `RESTRICTED` – only first party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
211
211
|
#
|
@@ -218,7 +218,7 @@ module StytchB2B
|
|
218
218
|
# third_party_connected_apps_allowed_type::
|
219
219
|
# The authentication setting that sets the Organization's policy towards third party Connected Apps. The accepted values are:
|
220
220
|
#
|
221
|
-
# `ALL_ALLOWED` – any third party Connected App in the Project is permitted for use by Members.
|
221
|
+
# `ALL_ALLOWED` – the default setting, any third party Connected App in the Project is permitted for use by Members.
|
222
222
|
#
|
223
223
|
# `RESTRICTED` – only third party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
224
224
|
#
|
@@ -354,7 +354,7 @@ module StytchB2B
|
|
354
354
|
# sso_jit_provisioning::
|
355
355
|
# The authentication setting that controls the JIT provisioning of Members when authenticating via SSO. The accepted values are:
|
356
356
|
#
|
357
|
-
# `ALL_ALLOWED` – new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
357
|
+
# `ALL_ALLOWED` – the default setting, new Members will be automatically provisioned upon successful authentication via any of the Organization's `sso_active_connections`.
|
358
358
|
#
|
359
359
|
# `RESTRICTED` – only new Members with SSO logins that comply with `sso_jit_provisioning_allowed_connections` can be provisioned upon authentication.
|
360
360
|
#
|
@@ -382,7 +382,7 @@ module StytchB2B
|
|
382
382
|
#
|
383
383
|
# `RESTRICTED` – only new Members with verified emails that comply with `email_allowed_domains` can be provisioned upon authentication via Email Magic Link or OAuth.
|
384
384
|
#
|
385
|
-
# `NOT_ALLOWED` –
|
385
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning via Email Magic Link and OAuth.
|
386
386
|
#
|
387
387
|
#
|
388
388
|
# If this field is provided and a session header is passed into the request, the Member Session must have permission to perform the `update.settings.email-jit-provisioning` action on the `stytch.organization` Resource.
|
@@ -456,7 +456,7 @@ module StytchB2B
|
|
456
456
|
#
|
457
457
|
# `RESTRICTED` – only new Members with tenants in `allowed_oauth_tenants` can JIT provision via tenant.
|
458
458
|
#
|
459
|
-
# `NOT_ALLOWED` –
|
459
|
+
# `NOT_ALLOWED` – the default setting, disables JIT provisioning by OAuth Tenant.
|
460
460
|
#
|
461
461
|
#
|
462
462
|
# If this field is provided and a session header is passed into the request, the Member Session must have permission to perform the `update.settings.oauth-tenant-jit-provisioning` action on the `stytch.organization` Resource.
|
@@ -472,7 +472,7 @@ module StytchB2B
|
|
472
472
|
# first_party_connected_apps_allowed_type::
|
473
473
|
# The authentication setting that sets the Organization's policy towards first party Connected Apps. The accepted values are:
|
474
474
|
#
|
475
|
-
# `ALL_ALLOWED` – any first party Connected App in the Project is permitted for use by Members.
|
475
|
+
# `ALL_ALLOWED` – the default setting, any first party Connected App in the Project is permitted for use by Members.
|
476
476
|
#
|
477
477
|
# `RESTRICTED` – only first party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
478
478
|
#
|
@@ -485,7 +485,7 @@ module StytchB2B
|
|
485
485
|
# third_party_connected_apps_allowed_type::
|
486
486
|
# The authentication setting that sets the Organization's policy towards third party Connected Apps. The accepted values are:
|
487
487
|
#
|
488
|
-
# `ALL_ALLOWED` – any third party Connected App in the Project is permitted for use by Members.
|
488
|
+
# `ALL_ALLOWED` – the default setting, any third party Connected App in the Project is permitted for use by Members.
|
489
489
|
#
|
490
490
|
# `RESTRICTED` – only third party Connected Apps with IDs in `allowed_first_party_connected_apps` can be used by Members.
|
491
491
|
#
|
@@ -1507,6 +1507,8 @@ module StytchB2B
|
|
1507
1507
|
# The member will receive an Email Magic Link that expires in 5 minutes. If they do not verify their new email address in that timeframe, the email
|
1508
1508
|
# will be freed up for other members to use.
|
1509
1509
|
#
|
1510
|
+
# The Magic Link will redirect to your `login_redirect_url` (or the configured default if one isn't provided), and you should invoke the [Authenticate Magic Link](https://stytch.com/docs/b2b/api/authenticate-magic-link) endpoint as normal to complete the flow.
|
1511
|
+
#
|
1510
1512
|
# == Parameters:
|
1511
1513
|
# organization_id::
|
1512
1514
|
# Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value. You may also use the organization_slug here as a convenience.
|
@@ -1515,7 +1517,7 @@ module StytchB2B
|
|
1515
1517
|
# Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform operations on a Member, so be sure to preserve this value. You may use an external_id here if one is set for the member.
|
1516
1518
|
# The type of this field is +String+.
|
1517
1519
|
# email_address::
|
1518
|
-
# The email address
|
1520
|
+
# The new email address for the Member.
|
1519
1521
|
# The type of this field is +String+.
|
1520
1522
|
# login_redirect_url::
|
1521
1523
|
# The URL that the Member clicks from the login Email Magic Link. This URL should be an endpoint in the backend server that
|
data/lib/stytch/b2b_otp.rb
CHANGED
@@ -38,14 +38,16 @@ module StytchB2B
|
|
38
38
|
#
|
39
39
|
# If a Member has a phone number and is enrolled in MFA, then after a successful primary authentication event (e.g. [email magic link](https://stytch.com/docs/b2b/api/authenticate-magic-link) or [SSO](https://stytch.com/docs/b2b/api/sso-authenticate) login is complete), an SMS OTP will automatically be sent to their phone number. In that case, this endpoint should only be used for subsequent authentication events, such as prompting a Member for an OTP again after a period of inactivity.
|
40
40
|
#
|
41
|
-
#
|
41
|
+
# If the Member already has an active MFA factor, then passing an intermediate session token, session token, or session JWT with the existing MFA factor on it is required to prevent bypassing MFA.
|
42
|
+
#
|
43
|
+
# Otherwise, passing an intermediate session token, session token, or session JWT is not required, but if passed must match the `member_id` passed.
|
42
44
|
#
|
43
45
|
# ### Cost to send SMS OTP
|
44
46
|
# Before configuring SMS or WhatsApp OTPs, please review how Stytch [bills the costs of international OTPs](https://stytch.com/pricing) and understand how to protect your app against [toll fraud](https://stytch.com/docs/guides/passcodes/toll-fraud/overview).
|
45
47
|
#
|
46
48
|
# Even when international SMS is enabled, we do not support sending SMS to countries on our [Unsupported countries list](https://stytch.com/docs/guides/passcodes/unsupported-countries).
|
47
49
|
#
|
48
|
-
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via
|
50
|
+
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via the [Dashboard](https://stytch.com/dashboard/country-code-allowlists) or [Programmatic Workspace Actions](https://stytch.com/docs/workspace-management/pwa/set-allowed-country-codes), and [add credit card details](https://stytch.com/dashboard/settings/billing) to your account.
|
49
51
|
#
|
50
52
|
# == Parameters:
|
51
53
|
# organization_id::
|
data/lib/stytch/b2b_passwords.rb
CHANGED
@@ -623,6 +623,9 @@ module StytchB2B
|
|
623
623
|
#
|
624
624
|
# == Returns:
|
625
625
|
# An object with the following fields:
|
626
|
+
# request_id::
|
627
|
+
# Globally unique UUID that is returned with every API call. This value is important to log for debugging purposes; we may ask for this value to help identify a specific API call when helping you debug an issue.
|
628
|
+
# The type of this field is +String+.
|
626
629
|
# status_code::
|
627
630
|
# The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors.
|
628
631
|
# The type of this field is +Integer+.
|
data/lib/stytch/b2b_rbac.rb
CHANGED
@@ -20,7 +20,7 @@ module StytchB2B
|
|
20
20
|
#
|
21
21
|
# When using the backend SDKs, the RBAC Policy will be cached to allow for local evaluations, eliminating the need for an extra request to Stytch. The policy will be refreshed if an authorization check is requested and the RBAC policy was last updated more than 5 minutes ago.
|
22
22
|
#
|
23
|
-
# Resources and Roles can be created and managed within the [RBAC page](https://stytch.com/
|
23
|
+
# Resources and Roles can be created and managed within the [RBAC page](https://stytch.com/dashboard/rbac) in the Dashboard.
|
24
24
|
# Additionally, [Role assignment](https://stytch.com/docs/b2b/guides/rbac/role-assignment) can be programmatically managed through certain Stytch API endpoints.
|
25
25
|
#
|
26
26
|
# Check out the [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview) to learn more about Stytch's RBAC permissioning model.
|
@@ -36,7 +36,7 @@ module StytchB2B
|
|
36
36
|
# The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors.
|
37
37
|
# The type of this field is +Integer+.
|
38
38
|
# policy::
|
39
|
-
# The RBAC Policy document that contains all defined Roles and Resources – which are managed in the [Dashboard](https://stytch.com/
|
39
|
+
# The RBAC Policy document that contains all defined Roles and Resources – which are managed in the [Dashboard](https://stytch.com/dashboard/rbac). Read more about these entities and how they work in our [RBAC overview](https://stytch.com/docs/b2b/guides/rbac/overview).
|
40
40
|
# The type of this field is nilable +Policy+ (+object+).
|
41
41
|
def policy
|
42
42
|
headers = {}
|
data/lib/stytch/b2b_sessions.rb
CHANGED
@@ -164,7 +164,7 @@ module StytchB2B
|
|
164
164
|
# The type of this field is +Integer+.
|
165
165
|
# verdict::
|
166
166
|
# If an `authorization_check` is provided in the request and the check succeeds, this field will return
|
167
|
-
#
|
167
|
+
# information about why the Member was granted permission.
|
168
168
|
# The type of this field is nilable +AuthorizationVerdict+ (+object+).
|
169
169
|
def authenticate(
|
170
170
|
session_token: nil,
|
@@ -417,7 +417,7 @@ module StytchB2B
|
|
417
417
|
post_request('/v1/b2b/sessions/exchange_access_token', request, headers)
|
418
418
|
end
|
419
419
|
|
420
|
-
# Exchange an auth token issued by a trusted identity provider for a Stytch session. You must first register a Trusted Auth Token profile in the Stytch dashboard [here](https://stytch.com/
|
420
|
+
# Exchange an auth token issued by a trusted identity provider for a Stytch session. You must first register a Trusted Auth Token profile in the Stytch dashboard [here](https://stytch.com/dashboard/trusted-auth-tokens). If a session token or session JWT is provided, it will add the trusted auth token as an authentication factor to the existing session.
|
421
421
|
#
|
422
422
|
# == Parameters:
|
423
423
|
# organization_id::
|
@@ -504,7 +504,7 @@ module StytchB2B
|
|
504
504
|
end
|
505
505
|
|
506
506
|
# Migrate a session from an external OIDC compliant endpoint.
|
507
|
-
# Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](https://stytch.com/
|
507
|
+
# Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](https://stytch.com/dashboard/migrations), and then perform a lookup using the `session_token`.
|
508
508
|
# If the response contains a valid email address, Stytch will attempt to match that email address with an existing Member in your Organization and create a Stytch Session.
|
509
509
|
# You will need to create the member before using this endpoint.
|
510
510
|
#
|
data/lib/stytch/b2b_totps.rb
CHANGED
@@ -18,7 +18,9 @@ module StytchB2B
|
|
18
18
|
|
19
19
|
# Create a new TOTP instance for a Member. The Member can use the authenticator application of their choice to scan the QR code or enter the secret.
|
20
20
|
#
|
21
|
-
#
|
21
|
+
# If the Member already has an active MFA factor, then passing an intermediate session token, session token, or session JWT with the existing MFA factor on it is required to prevent bypassing MFA.
|
22
|
+
#
|
23
|
+
# Otherwise, passing an intermediate session token, session token, or session JWT is not required, but if passed must match the `member_id` passed.
|
22
24
|
#
|
23
25
|
# == Parameters:
|
24
26
|
# organization_id::
|
data/lib/stytch/client.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
|
+
# !!!
|
2
|
+
# WARNING: This file is autogenerated
|
3
|
+
# Only modify code within MANUAL() sections
|
4
|
+
# or your changes may be overwritten later!
|
5
|
+
# !!!
|
6
|
+
|
1
7
|
# frozen_string_literal: true
|
2
8
|
|
3
9
|
require_relative 'connected_apps'
|
4
10
|
require_relative 'crypto_wallets'
|
5
11
|
require_relative 'fraud'
|
12
|
+
require_relative 'idp'
|
6
13
|
require_relative 'impersonation'
|
7
14
|
require_relative 'm2m'
|
8
15
|
require_relative 'magic_links'
|
@@ -10,6 +17,8 @@ require_relative 'oauth'
|
|
10
17
|
require_relative 'otps'
|
11
18
|
require_relative 'passwords'
|
12
19
|
require_relative 'project'
|
20
|
+
require_relative 'rbac'
|
21
|
+
require_relative 'rbac_local'
|
13
22
|
require_relative 'sessions'
|
14
23
|
require_relative 'totps'
|
15
24
|
require_relative 'users'
|
@@ -19,7 +28,7 @@ module Stytch
|
|
19
28
|
class Client
|
20
29
|
ENVIRONMENTS = %i[live test].freeze
|
21
30
|
|
22
|
-
attr_reader :connected_app, :crypto_wallets, :fraud, :impersonation, :m2m, :magic_links, :oauth, :otps, :passwords, :project, :sessions, :totps, :users, :webauthn
|
31
|
+
attr_reader :connected_app, :crypto_wallets, :fraud, :impersonation, :m2m, :magic_links, :oauth, :otps, :passwords, :project, :rbac, :sessions, :totps, :users, :webauthn, :idp
|
23
32
|
|
24
33
|
def initialize(project_id:, secret:, env: nil, fraud_env: nil, &block)
|
25
34
|
@api_host = api_host(env, project_id)
|
@@ -30,6 +39,10 @@ module Stytch
|
|
30
39
|
|
31
40
|
create_connection(&block)
|
32
41
|
|
42
|
+
rbac = Stytch::RBAC.new(@connection)
|
43
|
+
@policy_cache = Stytch::PolicyCache.new(rbac_client: rbac)
|
44
|
+
@idp = Stytch::IDP.new(@connection, @project_id, @policy_cache)
|
45
|
+
|
33
46
|
@connected_app = Stytch::ConnectedApp.new(@connection)
|
34
47
|
@crypto_wallets = Stytch::CryptoWallets.new(@connection)
|
35
48
|
@fraud = Stytch::Fraud.new(@fraud_connection)
|
@@ -40,7 +53,8 @@ module Stytch
|
|
40
53
|
@otps = Stytch::OTPs.new(@connection)
|
41
54
|
@passwords = Stytch::Passwords.new(@connection)
|
42
55
|
@project = Stytch::Project.new(@connection)
|
43
|
-
@
|
56
|
+
@rbac = Stytch::RBAC.new(@connection)
|
57
|
+
@sessions = Stytch::Sessions.new(@connection, @project_id, @policy_cache)
|
44
58
|
@totps = Stytch::TOTPs.new(@connection)
|
45
59
|
@users = Stytch::Users.new(@connection)
|
46
60
|
@webauthn = Stytch::WebAuthn.new(@connection)
|
data/lib/stytch/fraud.rb
CHANGED
@@ -28,14 +28,17 @@ module Stytch
|
|
28
28
|
@connection = connection
|
29
29
|
end
|
30
30
|
|
31
|
-
# Lookup the associated fingerprint for the `telemetry_id` returned from the `GetTelemetryID()` function.
|
31
|
+
# Lookup the associated fingerprint for the `telemetry_id` returned from the `GetTelemetryID()` function.
|
32
|
+
# Learn more about the different fingerprint types and verdicts in our [DFP guide](https://stytch.com/docs/fraud/guides/device-fingerprinting/overview).
|
32
33
|
#
|
33
|
-
#
|
34
|
+
# You can make a decision based on the recommended `verdict` in the response:
|
34
35
|
# * `ALLOW` - This is a known valid device grouping or device profile that is part of the default `ALLOW` listed set of known devices by Stytch. This grouping is made up of verified device profiles that match the characteristics of known/authentic traffic origins.
|
35
36
|
# * `BLOCK` - This is a known bad or malicious device profile that is undesirable and should be blocked from completing the privileged action in question.
|
36
37
|
# * `CHALLENGE` - This is an unknown or potentially malicious device that should be put through increased friction such as 2FA or other forms of extended user verification before allowing the privileged action to proceed.
|
37
38
|
#
|
38
|
-
# If the `telemetry_id` is not found,
|
39
|
+
# If the `telemetry_id` is expired or not found, this endpoint returns a 404 `telemetry_id_not_found` [error](https://stytch.com/docs/fraud/api/errors/404#telemetry_id_not_found).
|
40
|
+
# We recommend treating 404 errors as a `BLOCK`, since it could be a sign of an attacker trying to bypass DFP protections.
|
41
|
+
# See [Attacker-controlled telemetry IDs](https://stytch.com/docs/fraud/guides/device-fingerprinting/integration-steps/test-your-integration#attacker-controlled-telemetry-ids) for more information.
|
39
42
|
#
|
40
43
|
# == Parameters:
|
41
44
|
# telemetry_id::
|
data/lib/stytch/idp.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
require 'json/jwt'
|
5
|
+
require_relative 'errors'
|
6
|
+
require_relative 'request_helper'
|
7
|
+
require_relative 'rbac_local'
|
8
|
+
|
9
|
+
module Stytch
|
10
|
+
class IDP
|
11
|
+
include Stytch::RequestHelper
|
12
|
+
|
13
|
+
def initialize(connection, project_id, policy_cache)
|
14
|
+
@connection = connection
|
15
|
+
@project_id = project_id
|
16
|
+
@policy_cache = policy_cache
|
17
|
+
@non_custom_claim_keys = %w[
|
18
|
+
aud
|
19
|
+
exp
|
20
|
+
iat
|
21
|
+
iss
|
22
|
+
jti
|
23
|
+
nbf
|
24
|
+
sub
|
25
|
+
active
|
26
|
+
client_id
|
27
|
+
request_id
|
28
|
+
scope
|
29
|
+
status_code
|
30
|
+
token_type
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Introspects a token JWT from an authorization code response.
|
35
|
+
# Access tokens are JWTs signed with the project's JWKs. Refresh tokens are opaque tokens.
|
36
|
+
# Access tokens contain a standard set of claims as well as any custom claims generated from templates.
|
37
|
+
#
|
38
|
+
# == Parameters:
|
39
|
+
# token::
|
40
|
+
# The access token (or refresh token) to introspect.
|
41
|
+
# The type of this field is +String+.
|
42
|
+
# client_id::
|
43
|
+
# The ID of the client.
|
44
|
+
# The type of this field is +String+.
|
45
|
+
# client_secret::
|
46
|
+
# The secret of the client.
|
47
|
+
# The type of this field is nilable +String+.
|
48
|
+
# token_type_hint::
|
49
|
+
# A hint on what the token contains. Valid fields are 'access_token' and 'refresh_token'.
|
50
|
+
# The type of this field is +String+.
|
51
|
+
# authorization_check::
|
52
|
+
# Optional authorization check object.
|
53
|
+
# The type of this field is nilable +Hash+.
|
54
|
+
#
|
55
|
+
# == Returns:
|
56
|
+
# An object with the following fields:
|
57
|
+
# subject::
|
58
|
+
# The subject of the token.
|
59
|
+
# The type of this field is +String+.
|
60
|
+
# scope::
|
61
|
+
# The scope of the token.
|
62
|
+
# The type of this field is +String+.
|
63
|
+
# audience::
|
64
|
+
# The audience of the token.
|
65
|
+
# The type of this field is +String+.
|
66
|
+
# expires_at::
|
67
|
+
# The expiration time of the token.
|
68
|
+
# The type of this field is +Integer+.
|
69
|
+
# issued_at::
|
70
|
+
# The issued at time of the token.
|
71
|
+
# The type of this field is +Integer+.
|
72
|
+
# issuer::
|
73
|
+
# The issuer of the token.
|
74
|
+
# The type of this field is +String+.
|
75
|
+
# not_before::
|
76
|
+
# The not before time of the token.
|
77
|
+
# The type of this field is +Integer+.
|
78
|
+
# token_type::
|
79
|
+
# The type of the token.
|
80
|
+
# The type of this field is +String+.
|
81
|
+
# custom_claims::
|
82
|
+
# Custom claims in the token.
|
83
|
+
# The type of this field is +Hash+.
|
84
|
+
def introspect_token_network(
|
85
|
+
token:,
|
86
|
+
client_id:,
|
87
|
+
client_secret: nil,
|
88
|
+
token_type_hint: 'access_token',
|
89
|
+
authorization_check: nil
|
90
|
+
)
|
91
|
+
headers = {}
|
92
|
+
data = {
|
93
|
+
'token' => token,
|
94
|
+
'client_id' => client_id,
|
95
|
+
'token_type_hint' => token_type_hint
|
96
|
+
}
|
97
|
+
data['client_secret'] = client_secret unless client_secret.nil?
|
98
|
+
|
99
|
+
url = @connection.url_prefix + '/v1/oauth2/introspect'
|
100
|
+
res = post_request(url, data, headers)
|
101
|
+
|
102
|
+
jwt_response = res
|
103
|
+
return nil unless jwt_response['active']
|
104
|
+
|
105
|
+
custom_claims = res.reject { |k, _| @non_custom_claim_keys.include?(k) }
|
106
|
+
scope = jwt_response['scope']
|
107
|
+
|
108
|
+
if authorization_check
|
109
|
+
@policy_cache.perform_scope_authorization_check(
|
110
|
+
token_scopes: scope.split,
|
111
|
+
authorization_check: authorization_check
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
{
|
116
|
+
'subject' => jwt_response['sub'],
|
117
|
+
'scope' => jwt_response['scope'],
|
118
|
+
'audience' => jwt_response['aud'],
|
119
|
+
'expires_at' => jwt_response['exp'],
|
120
|
+
'issued_at' => jwt_response['iat'],
|
121
|
+
'issuer' => jwt_response['iss'],
|
122
|
+
'not_before' => jwt_response['nbf'],
|
123
|
+
'token_type' => jwt_response['token_type'],
|
124
|
+
'custom_claims' => custom_claims
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Introspects a token JWT from an authorization code response.
|
129
|
+
# Access tokens are JWTs signed with the project's JWKs. Refresh tokens are opaque tokens.
|
130
|
+
# Access tokens contain a standard set of claims as well as any custom claims generated from templates.
|
131
|
+
#
|
132
|
+
# == Parameters:
|
133
|
+
# access_token::
|
134
|
+
# The access token (or refresh token) to introspect.
|
135
|
+
# The type of this field is +String+.
|
136
|
+
# authorization_check::
|
137
|
+
# Optional authorization check object.
|
138
|
+
# The type of this field is nilable +Hash+.
|
139
|
+
#
|
140
|
+
# == Returns:
|
141
|
+
# An object with the following fields:
|
142
|
+
# subject::
|
143
|
+
# The subject of the token.
|
144
|
+
# The type of this field is +String+.
|
145
|
+
# scope::
|
146
|
+
# The scope of the token.
|
147
|
+
# The type of this field is +String+.
|
148
|
+
# audience::
|
149
|
+
# The audience of the token.
|
150
|
+
# The type of this field is +String+.
|
151
|
+
# expires_at::
|
152
|
+
# The expiration time of the token.
|
153
|
+
# The type of this field is +Integer+.
|
154
|
+
# issued_at::
|
155
|
+
# The issued at time of the token.
|
156
|
+
# The type of this field is +Integer+.
|
157
|
+
# issuer::
|
158
|
+
# The issuer of the token.
|
159
|
+
# The type of this field is +String+.
|
160
|
+
# not_before::
|
161
|
+
# The not before time of the token.
|
162
|
+
# The type of this field is +Integer+.
|
163
|
+
# token_type::
|
164
|
+
# The type of the token.
|
165
|
+
# The type of this field is +String+.
|
166
|
+
# custom_claims::
|
167
|
+
# Custom claims in the token.
|
168
|
+
# The type of this field is +Hash+.
|
169
|
+
def introspect_access_token_local(
|
170
|
+
access_token:,
|
171
|
+
authorization_check: nil
|
172
|
+
)
|
173
|
+
scope_claim = 'scope'
|
174
|
+
|
175
|
+
# Create a JWKS loader similar to other classes in the codebase
|
176
|
+
@cache_last_update = 0
|
177
|
+
jwks_loader = lambda do |options|
|
178
|
+
@cached_keys = nil if options[:invalidate] && @cache_last_update < Time.now.to_i - 300
|
179
|
+
if @cached_keys.nil?
|
180
|
+
@cached_keys = get_jwks(project_id: @project_id)
|
181
|
+
@cache_last_update = Time.now.to_i
|
182
|
+
end
|
183
|
+
@cached_keys
|
184
|
+
end
|
185
|
+
|
186
|
+
begin
|
187
|
+
decoded_jwt = JWT.decode(
|
188
|
+
access_token,
|
189
|
+
nil,
|
190
|
+
true,
|
191
|
+
{
|
192
|
+
algorithms: ['RS256'],
|
193
|
+
jwks: jwks_loader,
|
194
|
+
iss: ["stytch.com/#{@project_id}", @connection.url_prefix],
|
195
|
+
aud: @project_id
|
196
|
+
}
|
197
|
+
)[0]
|
198
|
+
|
199
|
+
generic_claims = decoded_jwt
|
200
|
+
custom_claims = generic_claims.reject { |k, _| @non_custom_claim_keys.include?(k) }
|
201
|
+
scope = generic_claims[scope_claim]
|
202
|
+
|
203
|
+
if authorization_check
|
204
|
+
@policy_cache.perform_scope_authorization_check(
|
205
|
+
token_scopes: scope.split,
|
206
|
+
authorization_check: authorization_check
|
207
|
+
)
|
208
|
+
end
|
209
|
+
|
210
|
+
{
|
211
|
+
'subject' => generic_claims['sub'],
|
212
|
+
'scope' => generic_claims[scope_claim],
|
213
|
+
'audience' => generic_claims['aud'],
|
214
|
+
'expires_at' => generic_claims['exp'],
|
215
|
+
'issued_at' => generic_claims['iat'],
|
216
|
+
'issuer' => generic_claims['iss'],
|
217
|
+
'not_before' => generic_claims['nbf'],
|
218
|
+
'token_type' => 'access_token',
|
219
|
+
'custom_claims' => custom_claims
|
220
|
+
}
|
221
|
+
rescue JWT::InvalidIssuerError
|
222
|
+
raise Stytch::JWTInvalidIssuerError
|
223
|
+
rescue JWT::InvalidAudError
|
224
|
+
raise Stytch::JWTInvalidAudienceError
|
225
|
+
rescue JWT::ExpiredSignature
|
226
|
+
raise Stytch::JWTExpiredSignatureError
|
227
|
+
rescue JWT::IncorrectAlgorithm
|
228
|
+
raise Stytch::JWTIncorrectAlgorithmError
|
229
|
+
rescue JWT::DecodeError
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Gets the JWKS for the project.
|
235
|
+
#
|
236
|
+
# == Parameters:
|
237
|
+
# project_id::
|
238
|
+
# The ID of the project.
|
239
|
+
# The type of this field is +String+.
|
240
|
+
#
|
241
|
+
# == Returns:
|
242
|
+
# The JWKS for the project.
|
243
|
+
# The type of this field is +Hash+.
|
244
|
+
def get_jwks(project_id:)
|
245
|
+
headers = {}
|
246
|
+
query_params = {}
|
247
|
+
request = request_with_query_params("/v1/sessions/jwks/#{project_id}", query_params)
|
248
|
+
get_request(request, headers)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
data/lib/stytch/otps.rb
CHANGED
@@ -130,7 +130,7 @@ module Stytch
|
|
130
130
|
# ### Cost to send SMS OTP
|
131
131
|
# Before configuring SMS or WhatsApp OTPs, please review how Stytch [bills the costs of international OTPs](https://stytch.com/pricing) and understand how to protect your app against [toll fraud](https://stytch.com/docs/guides/passcodes/toll-fraud/overview).
|
132
132
|
#
|
133
|
-
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via
|
133
|
+
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via the [Dashboard](https://stytch.com/dashboard/country-code-allowlists) or [Programmatic Workspace Actions](https://stytch.com/docs/workspace-management/pwa/set-allowed-country-codes), and [add credit card details](https://stytch.com/dashboard/settings/billing) to your account.
|
134
134
|
#
|
135
135
|
# Even when international SMS is enabled, we do not support sending SMS to countries on our [Unsupported countries list](https://stytch.com/docs/guides/passcodes/unsupported-countries).
|
136
136
|
#
|
@@ -212,7 +212,7 @@ module Stytch
|
|
212
212
|
# ### Cost to send SMS OTP
|
213
213
|
# Before configuring SMS or WhatsApp OTPs, please review how Stytch [bills the costs of international OTPs](https://stytch.com/pricing) and understand how to protect your app against [toll fraud](https://stytch.com/docs/guides/passcodes/toll-fraud/overview).
|
214
214
|
#
|
215
|
-
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via
|
215
|
+
# __Note:__ SMS to phone numbers outside of the US and Canada is disabled by default for customers who did not use SMS prior to October 2023. If you're interested in sending international SMS, please add those countries to your Project's allowlist via the [Dashboard](https://stytch.com/dashboard/country-code-allowlists) or [Programmatic Workspace Actions](https://stytch.com/docs/workspace-management/pwa/set-allowed-country-codes), and [add credit card details](https://stytch.com/dashboard/settings/billing) to your account.
|
216
216
|
#
|
217
217
|
# Even when international SMS is enabled, we do not support sending SMS to countries on our [Unsupported countries list](https://stytch.com/docs/guides/passcodes/unsupported-countries).
|
218
218
|
#
|
data/lib/stytch/passwords.rb
CHANGED
@@ -408,7 +408,7 @@ module Stytch
|
|
408
408
|
# login_redirect_url::
|
409
409
|
# The URL Stytch redirects to after the OAuth flow is completed for a user that already exists. This URL should be a route in your application which will run `oauth.authenticate` (see below) and finish the login.
|
410
410
|
#
|
411
|
-
# The URL must be configured as a Login URL in the [Redirect URL page](https://stytch.com/
|
411
|
+
# The URL must be configured as a Login URL in the [Redirect URL page](https://stytch.com/dashboard/redirect-urls). If the field is not specified, the default Login URL will be used.
|
412
412
|
# The type of this field is nilable +String+.
|
413
413
|
# locale::
|
414
414
|
# Used to determine which language to use when sending the user this delivery method. Parameter is a [IETF BCP 47 language tag](https://www.w3.org/International/articles/language-tags/), e.g. `"en"`.
|
data/lib/stytch/rbac.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# !!!
|
4
|
+
# WARNING: This file is autogenerated
|
5
|
+
# Only modify code within MANUAL() sections
|
6
|
+
# or your changes may be overwritten later!
|
7
|
+
# !!!
|
8
|
+
|
9
|
+
require_relative 'request_helper'
|
10
|
+
|
11
|
+
module Stytch
|
12
|
+
class RBAC
|
13
|
+
include Stytch::RequestHelper
|
14
|
+
|
15
|
+
def initialize(connection)
|
16
|
+
@connection = connection
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the active RBAC Policy for your current Stytch Project. An RBAC Policy is the canonical document that stores all defined Resources and Roles within your RBAC permissioning model.
|
20
|
+
#
|
21
|
+
# When using the backend SDKs, the RBAC Policy will be cached to allow for local evaluations, eliminating the need for an extra request to Stytch.
|
22
|
+
# The policy will be refreshed if an authorization check is requested and the RBAC policy was last updated more than 5 minutes ago.
|
23
|
+
#
|
24
|
+
# Resources and Roles can be created and managed within the [RBAC page](https://stytch.com/dashboard/rbac) in the Dashboard.
|
25
|
+
# Additionally, [Role assignment](https://stytch.com/docs/guides/rbac/role-assignment) can be programmatically managed through certain Stytch API endpoints.
|
26
|
+
#
|
27
|
+
# Check out the [RBAC overview](https://stytch.com/docs/guides/rbac/overview) to learn more about Stytch's RBAC permissioning model.
|
28
|
+
#
|
29
|
+
# == Parameters:
|
30
|
+
#
|
31
|
+
# == Returns:
|
32
|
+
# An object with the following fields:
|
33
|
+
# request_id::
|
34
|
+
# Globally unique UUID that is returned with every API call. This value is important to log for debugging purposes; we may ask for this value to help identify a specific API call when helping you debug an issue.
|
35
|
+
# The type of this field is +String+.
|
36
|
+
# status_code::
|
37
|
+
# The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors.
|
38
|
+
# The type of this field is +Integer+.
|
39
|
+
# policy::
|
40
|
+
# The RBAC Policy document that contains all defined Roles and Resources – which are managed in the [Dashboard](https://stytch.com/dashboard/rbac). Read more about these entities and how they work in our [RBAC overview](https://stytch.com/docs/guides/rbac/overview).
|
41
|
+
# The type of this field is nilable +Policy+ (+object+).
|
42
|
+
def policy
|
43
|
+
headers = {}
|
44
|
+
query_params = {}
|
45
|
+
request = request_with_query_params('/v1/rbac/policy', query_params)
|
46
|
+
get_request(request, headers)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/stytch/rbac_local.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require_relative 'errors'
|
4
4
|
require_relative 'request_helper'
|
5
5
|
|
6
|
-
module
|
6
|
+
module Stytch
|
7
7
|
class PolicyCache
|
8
8
|
def initialize(rbac_client:)
|
9
9
|
@rbac_client = rbac_client
|
@@ -31,8 +31,7 @@ module StytchB2B
|
|
31
31
|
subject_org_id:,
|
32
32
|
authorization_check:
|
33
33
|
)
|
34
|
-
|
35
|
-
raise Stytch::TenancyError.new(subject_org_id, request_org_id) if request_org_id != subject_org_id
|
34
|
+
raise Stytch::TenancyError.new(subject_org_id, authorization_check['organization_id']) if subject_org_id != authorization_check['organization_id']
|
36
35
|
|
37
36
|
policy = get_policy
|
38
37
|
|
@@ -54,5 +53,69 @@ module StytchB2B
|
|
54
53
|
# If we get here, we didn't find a matching permission
|
55
54
|
raise Stytch::PermissionError, authorization_check
|
56
55
|
end
|
56
|
+
|
57
|
+
# Performs an authorization check against the project's policy and a set of roles. If the
|
58
|
+
# check succeeds, this method will return. If the check fails, a PermissionError
|
59
|
+
# will be raised. This is used for role based authorization.
|
60
|
+
def perform_consumer_authorization_check(
|
61
|
+
subject_roles:,
|
62
|
+
authorization_check:
|
63
|
+
)
|
64
|
+
policy = get_policy
|
65
|
+
|
66
|
+
# For consumer authorization, we check roles without tenancy validation
|
67
|
+
for role in policy['roles']
|
68
|
+
next unless subject_roles.include?(role['role_id'])
|
69
|
+
|
70
|
+
for permission in role['permissions']
|
71
|
+
actions = permission['actions']
|
72
|
+
resource = permission['resource_id']
|
73
|
+
has_matching_action = actions.include?('*') || actions.include?(authorization_check['action'])
|
74
|
+
has_matching_resource = resource == authorization_check['resource_id']
|
75
|
+
if has_matching_action && has_matching_resource
|
76
|
+
return # Permission granted
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# If we get here, we didn't find a matching permission
|
82
|
+
raise Stytch::PermissionError, authorization_check
|
83
|
+
end
|
84
|
+
|
85
|
+
# Performs an authorization check against the project's policy and a set of scopes. If the
|
86
|
+
# check succeeds, this method will return. If the check fails, a PermissionError
|
87
|
+
# will be raised. This is used for OAuth-style scope-based authorization.
|
88
|
+
# authorization_check is an object with keys 'action' and 'resource_id'
|
89
|
+
def perform_scope_authorization_check(
|
90
|
+
token_scopes:,
|
91
|
+
authorization_check:
|
92
|
+
)
|
93
|
+
policy = get_policy
|
94
|
+
|
95
|
+
# For scope-based authorization, we check if any of the token scopes match policy scopes
|
96
|
+
# and if those scopes grant permission for the requested action/resource
|
97
|
+
action = authorization_check['action']
|
98
|
+
resource_id = authorization_check['resource_id']
|
99
|
+
|
100
|
+
# Check if any of the token scopes grant permission for this action/resource
|
101
|
+
for scope_obj in policy['scopes']
|
102
|
+
scope_name = scope_obj['scope']
|
103
|
+
next unless token_scopes.include?(scope_name)
|
104
|
+
|
105
|
+
# Check if this scope grants permission for the requested action/resource
|
106
|
+
for permission in scope_obj['permissions']
|
107
|
+
actions = permission['actions']
|
108
|
+
resource = permission['resource_id']
|
109
|
+
has_matching_action = actions.include?('*') || actions.include?(action)
|
110
|
+
has_matching_resource = resource == resource_id
|
111
|
+
if has_matching_action && has_matching_resource
|
112
|
+
return # Permission granted
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# If we get here, we didn't find a matching permission
|
118
|
+
raise Stytch::PermissionError, authorization_check
|
119
|
+
end
|
57
120
|
end
|
58
121
|
end
|
data/lib/stytch/sessions.rb
CHANGED
@@ -15,9 +15,10 @@ module Stytch
|
|
15
15
|
class Sessions
|
16
16
|
include Stytch::RequestHelper
|
17
17
|
|
18
|
-
def initialize(connection, project_id)
|
18
|
+
def initialize(connection, project_id, policy_cache)
|
19
19
|
@connection = connection
|
20
20
|
|
21
|
+
@policy_cache = policy_cache
|
21
22
|
@project_id = project_id
|
22
23
|
@cache_last_update = 0
|
23
24
|
@jwks_loader = lambda do |options|
|
@@ -81,6 +82,13 @@ module Stytch
|
|
81
82
|
#
|
82
83
|
# Custom claims made with reserved claims ("iss", "sub", "aud", "exp", "nbf", "iat", "jti") will be ignored. Total custom claims size cannot exceed four kilobytes.
|
83
84
|
# The type of this field is nilable +object+.
|
85
|
+
# authorization_check::
|
86
|
+
# If an `authorization_check` object is passed in, this endpoint will also check if the User is
|
87
|
+
# authorized to perform the given action on the given Resource. A User is authorized if they are assigned a Role with adequate permissions.
|
88
|
+
#
|
89
|
+
# If the User is not authorized to perform the specified action on the specified Resource, a 403 error will be thrown.
|
90
|
+
# Otherwise, the response will contain a list of Roles that satisfied the authorization check.
|
91
|
+
# The type of this field is nilable +AuthorizationCheck+ (+object+).
|
84
92
|
#
|
85
93
|
# == Returns:
|
86
94
|
# An object with the following fields:
|
@@ -105,11 +113,16 @@ module Stytch
|
|
105
113
|
# status_code::
|
106
114
|
# The HTTP status code of the response. Stytch follows standard HTTP response status code patterns, e.g. 2XX values equate to success, 3XX values are redirects, 4XX are client errors, and 5XX are server errors.
|
107
115
|
# The type of this field is +Integer+.
|
116
|
+
# verdict::
|
117
|
+
# If an `authorization_check` is provided in the request and the check succeeds, this field will return
|
118
|
+
# information about why the User was granted permission.
|
119
|
+
# The type of this field is nilable +AuthorizationVerdict+ (+object+).
|
108
120
|
def authenticate(
|
109
121
|
session_token: nil,
|
110
122
|
session_duration_minutes: nil,
|
111
123
|
session_jwt: nil,
|
112
|
-
session_custom_claims: nil
|
124
|
+
session_custom_claims: nil,
|
125
|
+
authorization_check: nil
|
113
126
|
)
|
114
127
|
headers = {}
|
115
128
|
request = {}
|
@@ -117,6 +130,7 @@ module Stytch
|
|
117
130
|
request[:session_duration_minutes] = session_duration_minutes unless session_duration_minutes.nil?
|
118
131
|
request[:session_jwt] = session_jwt unless session_jwt.nil?
|
119
132
|
request[:session_custom_claims] = session_custom_claims unless session_custom_claims.nil?
|
133
|
+
request[:authorization_check] = authorization_check unless authorization_check.nil?
|
120
134
|
|
121
135
|
post_request('/v1/sessions/authenticate', request, headers)
|
122
136
|
end
|
@@ -156,7 +170,7 @@ module Stytch
|
|
156
170
|
post_request('/v1/sessions/revoke', request, headers)
|
157
171
|
end
|
158
172
|
|
159
|
-
# Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](https://stytch.com/
|
173
|
+
# Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](https://stytch.com/dashboard), and then perform a lookup using the `session_token`. If the response contains a valid email address, Stytch will attempt to match that email address with an existing User and create a Stytch Session. You will need to create the user before using this endpoint.
|
160
174
|
#
|
161
175
|
# == Parameters:
|
162
176
|
# session_token::
|
@@ -326,7 +340,7 @@ module Stytch
|
|
326
340
|
get_request(request, headers)
|
327
341
|
end
|
328
342
|
|
329
|
-
# Exchange an auth token issued by a trusted identity provider for a Stytch session. You must first register a Trusted Auth Token profile in the Stytch dashboard [here](https://stytch.com/
|
343
|
+
# Exchange an auth token issued by a trusted identity provider for a Stytch session. You must first register a Trusted Auth Token profile in the Stytch dashboard [here](https://stytch.com/dashboard/trusted-auth-tokens). If a session token or session JWT is provided, it will add the trusted auth token as an authentication factor to the existing session.
|
330
344
|
#
|
331
345
|
# == Parameters:
|
332
346
|
# profile_id::
|
@@ -421,7 +435,8 @@ module Stytch
|
|
421
435
|
max_token_age_seconds: nil,
|
422
436
|
session_duration_minutes: nil,
|
423
437
|
session_custom_claims: nil,
|
424
|
-
clock_tolerance_seconds: nil
|
438
|
+
clock_tolerance_seconds: nil,
|
439
|
+
authorization_check: nil
|
425
440
|
)
|
426
441
|
max_token_age_seconds = 300 if max_token_age_seconds.nil?
|
427
442
|
clock_tolerance_seconds = 0 if clock_tolerance_seconds.nil?
|
@@ -430,28 +445,32 @@ module Stytch
|
|
430
445
|
return authenticate(
|
431
446
|
session_jwt: session_jwt,
|
432
447
|
session_duration_minutes: session_duration_minutes,
|
433
|
-
session_custom_claims: session_custom_claims
|
448
|
+
session_custom_claims: session_custom_claims,
|
449
|
+
authorization_check: authorization_check
|
434
450
|
)
|
435
451
|
end
|
436
452
|
|
437
453
|
session = authenticate_jwt_local(
|
438
454
|
session_jwt,
|
439
455
|
max_token_age_seconds: max_token_age_seconds,
|
440
|
-
clock_tolerance_seconds: clock_tolerance_seconds
|
456
|
+
clock_tolerance_seconds: clock_tolerance_seconds,
|
457
|
+
authorization_check: authorization_check
|
441
458
|
)
|
442
459
|
return session unless session.nil?
|
443
460
|
|
444
461
|
authenticate(
|
445
462
|
session_jwt: session_jwt,
|
446
463
|
session_duration_minutes: session_duration_minutes,
|
447
|
-
session_custom_claims: session_custom_claims
|
464
|
+
session_custom_claims: session_custom_claims,
|
465
|
+
authorization_check: authorization_check
|
448
466
|
)
|
449
467
|
rescue StandardError
|
450
468
|
# JWT could not be verified locally. Check with the Stytch API.
|
451
469
|
authenticate(
|
452
470
|
session_jwt: session_jwt,
|
453
471
|
session_duration_minutes: session_duration_minutes,
|
454
|
-
session_custom_claims: session_custom_claims
|
472
|
+
session_custom_claims: session_custom_claims,
|
473
|
+
authorization_check: authorization_check
|
455
474
|
)
|
456
475
|
end
|
457
476
|
|
@@ -461,7 +480,7 @@ module Stytch
|
|
461
480
|
# This method never authenticates a JWT directly with the API
|
462
481
|
# If max_token_age_seconds is not supplied 300 seconds will be used as the default.
|
463
482
|
# If clock_tolerance_seconds is not supplied 0 seconds will be used as the default.
|
464
|
-
def authenticate_jwt_local(session_jwt, max_token_age_seconds: nil, clock_tolerance_seconds: nil)
|
483
|
+
def authenticate_jwt_local(session_jwt, max_token_age_seconds: nil, clock_tolerance_seconds: nil, authorization_check: nil)
|
465
484
|
max_token_age_seconds = 300 if max_token_age_seconds.nil?
|
466
485
|
clock_tolerance_seconds = 0 if clock_tolerance_seconds.nil?
|
467
486
|
|
@@ -488,6 +507,14 @@ module Stytch
|
|
488
507
|
raise JWTIncorrectAlgorithmError
|
489
508
|
end
|
490
509
|
|
510
|
+
# Do the auth check - intentionally don't rescue errors from here
|
511
|
+
if authorization_check
|
512
|
+
@policy_cache.perform_consumer_authorization_check(
|
513
|
+
subject_roles: session['roles'],
|
514
|
+
authorization_check: authorization_check
|
515
|
+
)
|
516
|
+
end
|
517
|
+
|
491
518
|
session
|
492
519
|
end
|
493
520
|
|
data/lib/stytch/version.rb
CHANGED
data/lib/stytch.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stytch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 10.
|
4
|
+
version: 10.26.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- stytch
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -126,6 +126,7 @@ files:
|
|
126
126
|
- lib/stytch.rb
|
127
127
|
- lib/stytch/b2b_client.rb
|
128
128
|
- lib/stytch/b2b_discovery.rb
|
129
|
+
- lib/stytch/b2b_idp.rb
|
129
130
|
- lib/stytch/b2b_impersonation.rb
|
130
131
|
- lib/stytch/b2b_magic_links.rb
|
131
132
|
- lib/stytch/b2b_oauth.rb
|
@@ -143,6 +144,7 @@ files:
|
|
143
144
|
- lib/stytch/crypto_wallets.rb
|
144
145
|
- lib/stytch/errors.rb
|
145
146
|
- lib/stytch/fraud.rb
|
147
|
+
- lib/stytch/idp.rb
|
146
148
|
- lib/stytch/impersonation.rb
|
147
149
|
- lib/stytch/m2m.rb
|
148
150
|
- lib/stytch/magic_links.rb
|
@@ -152,6 +154,7 @@ files:
|
|
152
154
|
- lib/stytch/otps.rb
|
153
155
|
- lib/stytch/passwords.rb
|
154
156
|
- lib/stytch/project.rb
|
157
|
+
- lib/stytch/rbac.rb
|
155
158
|
- lib/stytch/rbac_local.rb
|
156
159
|
- lib/stytch/request_helper.rb
|
157
160
|
- lib/stytch/sessions.rb
|