rodauth-oauth 0.0.6 → 0.4.1
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/CHANGELOG.md +167 -5
- data/README.md +43 -20
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +8 -5
- data/lib/rodauth/features/oauth.rb +534 -409
- data/lib/rodauth/features/oauth_http_mac.rb +6 -10
- data/lib/rodauth/features/oauth_jwt.rb +115 -70
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/features/oidc.rb +399 -0
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/ttl_store.rb +1 -1
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +34 -0
- data/templates/client_secret_field.str +4 -0
- data/templates/description_field.str +4 -0
- data/templates/homepage_url_field.str +4 -0
- data/templates/name_field.str +4 -0
- data/templates/new_oauth_application.str +10 -0
- data/templates/oauth_application.str +11 -0
- data/templates/oauth_applications.str +14 -0
- data/templates/oauth_tokens.str +49 -0
- data/templates/redirect_uri_field.str +4 -0
- data/templates/scope_field.str +10 -0
- metadata +24 -10
@@ -8,20 +8,16 @@ module Rodauth
|
|
8
8
|
def delete_suffix(suffix)
|
9
9
|
suffix = suffix.to_s
|
10
10
|
len = suffix.length
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
dup
|
15
|
-
end
|
11
|
+
return dup unless len.positive? && index(suffix, -len)
|
12
|
+
|
13
|
+
self[0...-len]
|
16
14
|
end
|
17
15
|
|
18
16
|
def delete_prefix(prefix)
|
19
17
|
prefix = prefix.to_s
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
dup
|
24
|
-
end
|
18
|
+
return dup unless rindex(prefix, 0)
|
19
|
+
|
20
|
+
self[prefix.length..-1]
|
25
21
|
end
|
26
22
|
end
|
27
23
|
end
|
@@ -6,7 +6,12 @@ module Rodauth
|
|
6
6
|
Feature.define(:oauth_jwt) do
|
7
7
|
depends :oauth
|
8
8
|
|
9
|
-
|
9
|
+
JWKS = OAuth::TtlStore.new
|
10
|
+
|
11
|
+
auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
|
12
|
+
auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
|
13
|
+
|
14
|
+
auth_value_method :oauth_jwt_token_issuer, nil
|
10
15
|
|
11
16
|
auth_value_method :oauth_application_jws_jwk_column, nil
|
12
17
|
|
@@ -19,6 +24,10 @@ module Rodauth
|
|
19
24
|
auth_value_method :oauth_jwt_jwe_algorithm, nil
|
20
25
|
auth_value_method :oauth_jwt_jwe_encryption_method, nil
|
21
26
|
|
27
|
+
# values used for rotating keys
|
28
|
+
auth_value_method :oauth_jwt_legacy_public_key, nil
|
29
|
+
auth_value_method :oauth_jwt_legacy_algorithm, nil
|
30
|
+
|
22
31
|
auth_value_method :oauth_jwt_jwe_copyright, nil
|
23
32
|
auth_value_method :oauth_jwt_audience, nil
|
24
33
|
|
@@ -28,10 +37,17 @@ module Rodauth
|
|
28
37
|
auth_value_methods(
|
29
38
|
:jwt_encode,
|
30
39
|
:jwt_decode,
|
31
|
-
:jwks_set
|
40
|
+
:jwks_set,
|
41
|
+
:last_account_login_at
|
32
42
|
)
|
33
43
|
|
34
|
-
|
44
|
+
route(:jwks) do |r|
|
45
|
+
next unless is_authorization_server?
|
46
|
+
|
47
|
+
r.get do
|
48
|
+
json_response_success({ keys: jwks_set }, true)
|
49
|
+
end
|
50
|
+
end
|
35
51
|
|
36
52
|
def require_oauth_authorization(*scopes)
|
37
53
|
authorization_required unless authorization_token
|
@@ -45,6 +61,12 @@ module Rodauth
|
|
45
61
|
|
46
62
|
private
|
47
63
|
|
64
|
+
unless method_defined?(:last_account_login_at)
|
65
|
+
def last_account_login_at
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
48
70
|
def authorization_token
|
49
71
|
return @authorization_token if defined?(@authorization_token)
|
50
72
|
|
@@ -57,8 +79,8 @@ module Rodauth
|
|
57
79
|
|
58
80
|
return unless jwt_token
|
59
81
|
|
60
|
-
return if jwt_token["iss"] != oauth_jwt_token_issuer ||
|
61
|
-
jwt_token["aud"] != oauth_jwt_audience ||
|
82
|
+
return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
|
83
|
+
(oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
|
62
84
|
!jwt_token["sub"]
|
63
85
|
|
64
86
|
jwt_token
|
@@ -78,9 +100,7 @@ module Rodauth
|
|
78
100
|
jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
|
79
101
|
jwk = oauth_application[oauth_application_jws_jwk_column]
|
80
102
|
|
81
|
-
if jwk
|
82
|
-
jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
|
83
|
-
end
|
103
|
+
jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
|
84
104
|
else
|
85
105
|
redirect_response_error("invalid_request_object")
|
86
106
|
end
|
@@ -95,8 +115,8 @@ module Rodauth
|
|
95
115
|
# [RFC7519] specification. The value of "aud" should be the value of
|
96
116
|
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
97
117
|
# [RFC8414].
|
98
|
-
claims.delete(
|
99
|
-
audience = claims.delete(
|
118
|
+
claims.delete("iss")
|
119
|
+
audience = claims.delete("aud")
|
100
120
|
|
101
121
|
redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
|
102
122
|
|
@@ -109,11 +129,17 @@ module Rodauth
|
|
109
129
|
|
110
130
|
# /token
|
111
131
|
|
112
|
-
def
|
132
|
+
def require_oauth_application
|
113
133
|
# requset authentication optional for assertions
|
114
|
-
return
|
134
|
+
return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
115
135
|
|
116
|
-
|
136
|
+
claims = jwt_decode(param("assertion"))
|
137
|
+
|
138
|
+
redirect_response_error("invalid_grant") unless claims
|
139
|
+
|
140
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
141
|
+
|
142
|
+
authorization_required unless @oauth_application
|
117
143
|
end
|
118
144
|
|
119
145
|
def validate_oauth_token_params
|
@@ -135,10 +161,6 @@ module Rodauth
|
|
135
161
|
def create_oauth_token_from_assertion
|
136
162
|
claims = jwt_decode(param("assertion"))
|
137
163
|
|
138
|
-
redirect_response_error("invalid_grant") unless claims
|
139
|
-
|
140
|
-
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
141
|
-
|
142
164
|
account = account_ds(claims["sub"]).first
|
143
165
|
|
144
166
|
redirect_response_error("invalid_client") unless oauth_application && account
|
@@ -154,26 +176,40 @@ module Rodauth
|
|
154
176
|
|
155
177
|
def generate_oauth_token(params = {}, should_generate_refresh_token = true)
|
156
178
|
create_params = {
|
157
|
-
oauth_grants_expires_in_column =>
|
179
|
+
oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_token_expires_in)
|
158
180
|
}.merge(params)
|
159
181
|
|
160
|
-
|
161
|
-
|
182
|
+
oauth_token = rescue_from_uniqueness_error do
|
183
|
+
if should_generate_refresh_token
|
184
|
+
refresh_token = oauth_unique_id_generator
|
162
185
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
186
|
+
if oauth_tokens_refresh_token_hash_column
|
187
|
+
create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
|
188
|
+
else
|
189
|
+
create_params[oauth_tokens_refresh_token_column] = refresh_token
|
190
|
+
end
|
167
191
|
end
|
192
|
+
|
193
|
+
_generate_oauth_token(create_params)
|
168
194
|
end
|
169
195
|
|
170
|
-
|
196
|
+
claims = jwt_claims(oauth_token)
|
197
|
+
|
198
|
+
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
199
|
+
# token data.
|
200
|
+
claims[:scope] = oauth_token[oauth_tokens_scopes_column]
|
171
201
|
|
172
|
-
|
202
|
+
token = jwt_encode(claims)
|
173
203
|
|
174
|
-
|
175
|
-
|
176
|
-
|
204
|
+
oauth_token[oauth_tokens_token_column] = token
|
205
|
+
oauth_token
|
206
|
+
end
|
207
|
+
|
208
|
+
def jwt_claims(oauth_token)
|
209
|
+
issued_at = Time.now.to_i
|
210
|
+
|
211
|
+
claims = {
|
212
|
+
iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
|
177
213
|
iat: issued_at, # issued at
|
178
214
|
#
|
179
215
|
# sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
|
@@ -184,23 +220,32 @@ module Rodauth
|
|
184
220
|
# owner is involved, such as the client credentials grant, the value
|
185
221
|
# of "sub" SHOULD correspond to an identifier the authorization
|
186
222
|
# server uses to indicate the client application.
|
223
|
+
sub: jwt_subject(oauth_token),
|
187
224
|
client_id: oauth_application[oauth_applications_client_id_column],
|
188
225
|
|
189
226
|
exp: issued_at + oauth_token_expires_in,
|
190
|
-
aud: oauth_jwt_audience
|
191
|
-
|
192
|
-
# one of the points of using jwt is avoiding database lookups, so we put here all relevant
|
193
|
-
# token data.
|
194
|
-
scope: oauth_token[oauth_tokens_scopes_column]
|
227
|
+
aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
|
195
228
|
}
|
196
229
|
|
197
|
-
|
230
|
+
claims[:auth_time] = last_account_login_at.to_i if last_account_login_at
|
198
231
|
|
199
|
-
|
200
|
-
|
232
|
+
claims
|
233
|
+
end
|
234
|
+
|
235
|
+
def jwt_subject(oauth_token)
|
236
|
+
case oauth_jwt_subject_type
|
237
|
+
when "public"
|
238
|
+
oauth_token[oauth_tokens_account_id_column]
|
239
|
+
when "pairwise"
|
240
|
+
id = oauth_token[oauth_tokens_account_id_column]
|
241
|
+
application_id = oauth_token[oauth_tokens_oauth_application_id_column]
|
242
|
+
Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
|
243
|
+
else
|
244
|
+
raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
|
245
|
+
end
|
201
246
|
end
|
202
247
|
|
203
|
-
def oauth_token_by_token(token
|
248
|
+
def oauth_token_by_token(token)
|
204
249
|
jwt_decode(token)
|
205
250
|
end
|
206
251
|
|
@@ -228,17 +273,11 @@ module Rodauth
|
|
228
273
|
def oauth_server_metadata_body(path)
|
229
274
|
metadata = super
|
230
275
|
metadata.merge! \
|
231
|
-
jwks_uri:
|
276
|
+
jwks_uri: jwks_url,
|
232
277
|
token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
|
233
278
|
metadata
|
234
279
|
end
|
235
280
|
|
236
|
-
def token_from_application?(oauth_token, oauth_application)
|
237
|
-
return super unless oauth_token["sub"] # naive check on whether it's a jwt token
|
238
|
-
|
239
|
-
oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
|
240
|
-
end
|
241
|
-
|
242
281
|
def _jwt_key
|
243
282
|
@_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
|
244
283
|
end
|
@@ -268,10 +307,10 @@ module Rodauth
|
|
268
307
|
|
269
308
|
# time-to-live
|
270
309
|
ttl = if response.key?("cache-control")
|
271
|
-
cache_control = response["
|
272
|
-
cache_control[/max-age=(\d+)/, 1]
|
310
|
+
cache_control = response["cache-control"]
|
311
|
+
cache_control[/max-age=(\d+)/, 1].to_i
|
273
312
|
elsif response.key?("expires")
|
274
|
-
Time.
|
313
|
+
Time.parse(response["expires"]).to_i - Time.now.to_i
|
275
314
|
end
|
276
315
|
|
277
316
|
[JSON.parse(response.body, symbolize_names: true), ttl]
|
@@ -279,7 +318,6 @@ module Rodauth
|
|
279
318
|
end
|
280
319
|
|
281
320
|
if defined?(JSON::JWT)
|
282
|
-
# :nocov:
|
283
321
|
|
284
322
|
def jwk_import(data)
|
285
323
|
JSON::JWK.new(data)
|
@@ -305,23 +343,27 @@ module Rodauth
|
|
305
343
|
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
|
306
344
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
307
345
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
346
|
+
if is_authorization_server?
|
347
|
+
if oauth_jwt_legacy_public_key
|
348
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
|
349
|
+
elsif jws_key
|
350
|
+
JSON::JWT.decode(token, jws_key)
|
351
|
+
end
|
352
|
+
elsif (jwks = auth_server_jwks_set)
|
353
|
+
JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
|
354
|
+
end
|
313
355
|
rescue JSON::JWT::Exception
|
314
356
|
nil
|
315
357
|
end
|
316
358
|
|
317
359
|
def jwks_set
|
318
|
-
[
|
360
|
+
@jwks_set ||= [
|
319
361
|
(JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
362
|
+
(JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
|
320
363
|
(JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
321
364
|
].compact
|
322
365
|
end
|
323
366
|
|
324
|
-
# :nocov:
|
325
367
|
elsif defined?(JWT)
|
326
368
|
|
327
369
|
# ruby-jwt
|
@@ -366,21 +408,30 @@ module Rodauth
|
|
366
408
|
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
|
367
409
|
# decrypt jwe
|
368
410
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
369
|
-
|
370
411
|
# decode jwt
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
412
|
+
if is_authorization_server?
|
413
|
+
if oauth_jwt_legacy_public_key
|
414
|
+
algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
415
|
+
JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first
|
416
|
+
elsif jws_key
|
417
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
|
418
|
+
end
|
419
|
+
elsif (jwks = auth_server_jwks_set)
|
420
|
+
algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
421
|
+
JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first
|
422
|
+
end
|
377
423
|
rescue JWT::DecodeError, JWT::JWKError
|
378
424
|
nil
|
379
425
|
end
|
380
426
|
|
381
427
|
def jwks_set
|
382
|
-
[
|
428
|
+
@jwks_set ||= [
|
383
429
|
(JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
|
430
|
+
(
|
431
|
+
if oauth_jwt_legacy_public_key
|
432
|
+
JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
|
433
|
+
end
|
434
|
+
),
|
384
435
|
(JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
|
385
436
|
].compact
|
386
437
|
end
|
@@ -411,11 +462,5 @@ module Rodauth
|
|
411
462
|
|
412
463
|
super
|
413
464
|
end
|
414
|
-
|
415
|
-
route(:oauth_jwks) do |r|
|
416
|
-
r.get do
|
417
|
-
json_response_success({ keys: jwks_set })
|
418
|
-
end
|
419
|
-
end
|
420
465
|
end
|
421
466
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "onelogin/ruby-saml"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_saml) do
|
7
|
+
depends :oauth
|
8
|
+
|
9
|
+
auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
|
10
|
+
auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
|
11
|
+
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
12
|
+
|
13
|
+
auth_value_method :oauth_saml_security_authn_requests_signed, false
|
14
|
+
auth_value_method :oauth_saml_security_metadata_signed, false
|
15
|
+
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
|
16
|
+
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
|
17
|
+
|
18
|
+
SAML_GRANT_TYPE = "http://oauth.net/grant_type/assertion/saml/2.0/bearer"
|
19
|
+
|
20
|
+
# /token
|
21
|
+
|
22
|
+
def require_oauth_application
|
23
|
+
# requset authentication optional for assertions
|
24
|
+
return super unless param("grant_type") == SAML_GRANT_TYPE && !param_or_nil("client_id")
|
25
|
+
|
26
|
+
# TODO: invalid grant
|
27
|
+
authorization_required unless saml_assertion
|
28
|
+
|
29
|
+
redirect_uri = saml_assertion.destination
|
30
|
+
|
31
|
+
@oauth_application = db[oauth_applications_table].where(
|
32
|
+
oauth_applications_homepage_url_column => saml_assertion.audiences,
|
33
|
+
oauth_applications_redirect_uri_column => redirect_uri
|
34
|
+
).first
|
35
|
+
|
36
|
+
# The Assertion's <Issuer> element MUST contain a unique identifier
|
37
|
+
# for the entity that issued the Assertion.
|
38
|
+
authorization_required unless saml_assertion.issuers.all? do |issuer|
|
39
|
+
issuer.start_with?(@oauth_application[oauth_applications_homepage_url_column])
|
40
|
+
end
|
41
|
+
|
42
|
+
authorization_required unless @oauth_application
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def secret_matches?(oauth_application, secret)
|
48
|
+
return super unless param_or_nil("assertion")
|
49
|
+
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def saml_assertion
|
54
|
+
return @saml_assertion if defined?(@saml_assertion)
|
55
|
+
|
56
|
+
@saml_assertion = begin
|
57
|
+
settings = OneLogin::RubySaml::Settings.new
|
58
|
+
settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
|
59
|
+
settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
|
60
|
+
settings.name_identifier_format = oauth_saml_name_identifier_format
|
61
|
+
settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
|
62
|
+
settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
|
63
|
+
settings.security[:digest_method] = oauth_saml_security_digest_method
|
64
|
+
settings.security[:signature_method] = oauth_saml_security_signature_method
|
65
|
+
|
66
|
+
response = OneLogin::RubySaml::Response.new(param("assertion"), settings: settings, skip_recipient_check: true)
|
67
|
+
|
68
|
+
return unless response.is_valid?
|
69
|
+
|
70
|
+
response
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_oauth_token_params
|
75
|
+
return super unless param("grant_type") == SAML_GRANT_TYPE
|
76
|
+
|
77
|
+
redirect_response_error("invalid_client") unless param_or_nil("assertion")
|
78
|
+
|
79
|
+
redirect_response_error("invalid_scope") unless check_valid_scopes?
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_oauth_token
|
83
|
+
if param("grant_type") == SAML_GRANT_TYPE
|
84
|
+
create_oauth_token_from_saml_assertion
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_oauth_token_from_saml_assertion
|
91
|
+
account = db[accounts_table].where(login_column => saml_assertion.nameid).first
|
92
|
+
|
93
|
+
redirect_response_error("invalid_client") unless oauth_application && account
|
94
|
+
|
95
|
+
create_params = {
|
96
|
+
oauth_tokens_account_id_column => account[account_id_column],
|
97
|
+
oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
98
|
+
oauth_tokens_scopes_column => (param_or_nil("scope") || oauth_application[oauth_applications_scopes_column])
|
99
|
+
}
|
100
|
+
|
101
|
+
generate_oauth_token(create_params, false)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|