roda-oauth 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 109babac0eef81698b84f827a65580e369fc75632436a4a35071de92a55c3658
4
+ data.tar.gz: e7bdb8b67d8ed5afc0a9577722e63bf98927db01fb009e2e43f0adf0a9091700
5
+ SHA512:
6
+ metadata.gz: 1e4e951c45f396c06a60c63000a9ce9203e34be111af11140ba5080155fedfd31165cfb399abf6aa4ae572d6f759b99a0f75cf5774f5c3493ab8e34c3e3b5e5b
7
+ data.tar.gz: 434f2e91fcd819bc4eb4014d5465bf21f752b72aa3751dd72a8615141eb9f17a8eaab4c5255c409c59840d97abb5046f116ba6392db0abeb20daf3d85551435c
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.0.1
4
+
5
+ Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
@@ -0,0 +1,137 @@
1
+ # Roda::Oauth
2
+
3
+
4
+ This is an extension to the `rodauth` gem which adds support for the [OAuth 2.0 protocol](https://tools.ietf.org/html/rfc6749).
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'roda-oauth'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install roda-oauth
21
+
22
+ ## Usage
23
+
24
+ This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
25
+
26
+ ```ruby
27
+ plugin :rodauth do
28
+ # enable it in the plugin
29
+ enable :login, :oauth
30
+ oauth_application_default_scope %w[profile.read]
31
+ oauth_application_scopes %w[profile.read profile.write]
32
+ end
33
+
34
+ # then, inside roda
35
+
36
+ route do |r|
37
+ r.rodauth
38
+
39
+ # public routes go here
40
+ # ...
41
+ # here you do your thing
42
+ # authenticated section is here
43
+
44
+ rodauth.require_authentication
45
+
46
+ # oauth will only kick in on ce you call #require_oauth_authorization
47
+
48
+ r.is "users" do
49
+ rodauth.require_oauth_authorization # defaults to profile.read
50
+ r.post do
51
+ rodauth.require_oauth_authorization("profile.write")
52
+ end
53
+ # ...
54
+ end
55
+
56
+ r.is "books" do
57
+ rodauth.require_oauth_authorization("books.read", "books.research")
58
+ r.get do
59
+ # ...
60
+ end
61
+ end
62
+ end
63
+ ```
64
+
65
+ You'll have to do a bit more boilerplate, so here's the instructions.
66
+
67
+ ### Example (TL;DR)
68
+
69
+ If you're familiar with the technology and want to skip the next paragraphs, just [check our roda example](https://gitlab.com/honeyryderchuck/roda-oauth/-/tree/master/examples/roda).
70
+
71
+ ### Database migrations
72
+
73
+ You have to generate database tables for Oauth applications, grants and tokens. In order for you to hit the ground running, [here's a set of migrations (using `sequel`) to generate the needed tables](https://gitlab.com/honeyryderchuck/roda-oauth/-/tree/master/test/migrate) (omit the first 2 if you already have account tables).
74
+
75
+ You can change column names or even use existing tables, however, be aware that you'll have to define new column accessors at the `rodauth` plugin declaration level. Let's say, for instance, you'd like to change the `oauth_grants` table name to `access_grants`, and it's `code` column to `authorization_code`; then, you'd have to do the following:
76
+
77
+ ```ruby
78
+ plugin :rodauth do
79
+ # enable it in the plugin
80
+ enable :login, :oauth
81
+ # ...
82
+ oauth_grants_table "access_grants"
83
+ oauth_grants_code_column "authorization_code"
84
+ end
85
+ ```
86
+
87
+ If you're starting from scratch though, the recommendation is to stick to the defaults.
88
+
89
+ ### HTML views
90
+
91
+ You'll have to generate HTML templates for the Oauth Authorization form.
92
+
93
+ The rodauth default setup expects the roda `render` plugin to be activated; by default, it expects a `views` directory to be defined in the project root folder. The Oauth Authorization template must be therefore defined there, and it should be called `oauth_authorize.(erb|str|...)` (read the [roda `render` plugin documentation](http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Render.html) for more info about HTML templating).
94
+
95
+ ### Endpoints
96
+
97
+ Once you set it up, by default, the following endpoints will be available:
98
+
99
+ * `GET /oauth-authorize`: Loads the OAuth authorization HTML form;
100
+ * `POST /oauth-authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
101
+ * `POST /oauth-token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
102
+
103
+ ### OAuth applications
104
+
105
+ This feature is **optional**, as not all authorization servers will want a full oauth applications dashboard. However, if you do and you don't want to do the work yourself, you can set it up in your roda app like this:
106
+
107
+ ```ruby
108
+ route do |r|
109
+ r.rodauth
110
+ # don't forget to authenticate to access the dashboard
111
+ rodauth.require_authentication
112
+ rodauth.oauth_applications
113
+ # ...
114
+ end
115
+ ```
116
+
117
+ This will define the following endpoints:
118
+
119
+ * `GET /oauth-applications`: returns the OAuth applications HTML dashboard;
120
+ * `GET /oauth-applications/{application_id}`: returns an OAuth application HTML page;
121
+ * `GET /oauth-applications/new`: returns a new OAuth application form;
122
+ * `POST /oauth-applications`: processes a new OAuth application request;
123
+
124
+ As in the OAuth authorization form example, you'll have to define the following HTML templates in order to use this feature:
125
+
126
+ * `oauth_applications.(erb|str|...)`: the list of OAuth applications;
127
+ * `oauth_application.(erb|str|...)`: the OAuth application page;
128
+ * `new_oauth_application.(erb|str|...)`: the new OAuth application form;
129
+
130
+ ## Development
131
+
132
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests, and `rake rubocop` to run thew linter.
133
+
134
+ ## Contributing
135
+
136
+ Bug reports and pull requests are welcome on Gitlab at https://gitlab.com/honeyryderchuck/roda-oauth.
137
+
@@ -0,0 +1,572 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth, :Oauth) do
5
+ SCOPES = %w[profile.read].freeze
6
+
7
+ depends :login
8
+
9
+ before "authorize"
10
+ after "authorize"
11
+ after "authorize_failure"
12
+
13
+ before "token"
14
+ after "token"
15
+
16
+ before "create_oauth_application"
17
+ after "create_oauth_application"
18
+
19
+ error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
20
+
21
+ error_flash "Please authorize to continue", "require_authorization"
22
+ error_flash "There was an error registering your oauth application", "create_oauth_application"
23
+ notice_flash "Your oauth application has been registered", "create_oauth_application"
24
+
25
+ view "oauth_authorize", "Authorize", "authorize"
26
+ view "oauth_applications", "Oauth Applications", "oauth_applications"
27
+ view "oauth_application", "Oauth Application", "oauth_application"
28
+ view "new_oauth_application", "New Oauth Application", "new_oauth_application"
29
+
30
+ auth_value_method :json_response_content_type, "application/json"
31
+
32
+ auth_value_method :oauth_grant_expires_in, 60 * 5 # 5 minutes
33
+ auth_value_method :oauth_token_expires_in, 60 * 60 # 60 minutes
34
+
35
+ # URL PARAMS
36
+
37
+ # Authorize / token
38
+ %w[grant_type code refresh_token client_id scope state redirect_uri scopes].each do |param|
39
+ auth_value_method :"#{param}_param", param
40
+ end
41
+
42
+ # Application
43
+ APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri].freeze
44
+ auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
45
+
46
+ (APPLICATION_REQUIRED_PARAMS + %w[client_id client_secret]).each do |param|
47
+ auth_value_method :"oauth_application_#{param}_param", param
48
+ end
49
+
50
+ # OAuth Token
51
+ auth_value_method :oauth_tokens_table, :oauth_tokens
52
+ auth_value_method :oauth_tokens_id_column, :id
53
+
54
+ %i[
55
+ oauth_application_id oauth_token_id oauth_grant_id
56
+ token refresh_token scopes
57
+ expires_in revoked_at
58
+ ].each do |column|
59
+ auth_value_method :"oauth_tokens_#{column}_column", column
60
+ end
61
+
62
+ # OAuth Grants
63
+ auth_value_method :oauth_grants_table, :oauth_grants
64
+ auth_value_method :oauth_grants_id_column, :id
65
+ %i[
66
+ account_id oauth_application_id
67
+ redirect_uri code scopes
68
+ expires_in revoked_at
69
+ ].each do |column|
70
+ auth_value_method :"oauth_grants_#{column}_column", column
71
+ end
72
+
73
+ auth_value_method :authorization_required_error_status, 401
74
+ auth_value_method :invalid_oauth_response_status, 400
75
+
76
+ # OAuth Applications
77
+ auth_value_method :oauth_applications_path, "oauth-applications"
78
+ auth_value_method :oauth_applications_table, :oauth_applications
79
+
80
+ auth_value_method :oauth_applications_id_column, :id
81
+ auth_value_method :oauth_applications_id_pattern, Integer
82
+
83
+ %i[
84
+ account_id
85
+ name description scopes
86
+ client_id client_secret
87
+ homepage_url redirect_uri
88
+ ].each do |column|
89
+ auth_value_method :"oauth_applications_#{column}_column", column
90
+ end
91
+
92
+ auth_value_method :oauth_application_default_scope, SCOPES.first
93
+ auth_value_method :oauth_application_scopes, SCOPES
94
+ auth_value_method :oauth_token_type, "Bearer"
95
+
96
+ auth_value_method :invalid_request, "Request is missing a required parameter"
97
+ auth_value_method :invalid_client, "Invalid client"
98
+ auth_value_method :unauthorized_client, "Unauthorized client"
99
+ auth_value_method :invalid_grant_type_message, "Invalid grant type"
100
+ auth_value_method :invalid_grant_message, "Invalid grant"
101
+ auth_value_method :invalid_scope_message, "Invalid scope"
102
+
103
+ auth_value_method :invalid_url_message, "Invalid URL"
104
+
105
+ auth_value_method :unique_error_message, "is already in use"
106
+ auth_value_method :null_error_message, "is not filled"
107
+
108
+ auth_value_methods(
109
+ :oauth_unique_id_generator,
110
+ :state,
111
+ :oauth_application,
112
+ :redirect_uri,
113
+ :client_id,
114
+ :scopes
115
+ )
116
+
117
+ redirect(:oauth_application) do |id|
118
+ "/#{oauth_applications_path}/#{id}"
119
+ end
120
+
121
+ redirect(:require_authorization) do
122
+ if logged_in?
123
+ oauth_authorize_path
124
+ else
125
+ login_redirect
126
+ end
127
+ end
128
+
129
+ auth_value_method :json_request_accept_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
130
+ auth_methods(:json_request?)
131
+
132
+ # Overrides logged_in?, so that a valid authorization token also authnenticates a request
133
+ def logged_in?
134
+ super || authorization_token
135
+ end
136
+
137
+ def json_request?
138
+ return @json_request if defined?(@json_request)
139
+
140
+ @json_request = request.get_header("HTTP_ACCEPT") =~ json_request_accept_regexp
141
+ end
142
+
143
+ attr_reader :oauth_application
144
+
145
+ def initialize(scope)
146
+ @scope = scope
147
+ end
148
+
149
+ def state
150
+ state = param(state_param)
151
+
152
+ return unless state && !state.empty?
153
+
154
+ state
155
+ end
156
+
157
+ def scopes
158
+ scopes = param(scopes_param)
159
+
160
+ return oauth_application_default_scope unless scopes && !scopes.empty?
161
+
162
+ scopes
163
+ end
164
+
165
+ def client_id
166
+ client_id = param(client_id_param)
167
+
168
+ return unless client_id && !client_id.empty?
169
+
170
+ client_id
171
+ end
172
+
173
+ def redirect_uri
174
+ redirect_uri = param(redirect_uri_param)
175
+
176
+ return oauth_application[oauth_applications_redirect_uri_column] unless redirect_uri && !redirect_uri.empty?
177
+
178
+ redirect_uri
179
+ end
180
+
181
+ def oauth_application
182
+ return @oauth_application if defined?(@oauth_application)
183
+
184
+ @oauth_application = begin
185
+ client_id = param(client_id_param)
186
+
187
+ return unless client_id
188
+
189
+ db[oauth_applications_table].filter(oauth_applications_client_id_column => client_id).first
190
+ end
191
+ end
192
+
193
+ def authorization_token
194
+ return @authorization_token if defined?(@authorization_token)
195
+
196
+ @authorization_token = begin
197
+ value = request.get_header("HTTP_AUTHORIZATION").to_s
198
+
199
+ scheme, token = value.split(" ", 2)
200
+
201
+ return unless scheme == "Bearer"
202
+
203
+ # check if there is a token
204
+ # check if token has not expired
205
+ # check if token has been revoked
206
+ db[oauth_tokens_table].where(oauth_tokens_token_column => token)
207
+ .where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
208
+ .where(oauth_tokens_revoked_at_column => nil)
209
+ .first
210
+ end
211
+ end
212
+
213
+ def require_oauth_authorization(*scopes)
214
+ authorization_required unless authorization_token
215
+
216
+ scopes << oauth_application_default_scope if scopes.empty?
217
+
218
+ token_scopes = authorization_token[:scopes].split(",")
219
+
220
+ authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
221
+ end
222
+
223
+ # /oauth-applications routes
224
+ def oauth_applications
225
+ request.on(oauth_applications_path) do
226
+ require_account
227
+
228
+ request.get "new" do
229
+ new_oauth_application_view
230
+ end
231
+ request.on(oauth_applications_id_pattern) do |id|
232
+ request.get do
233
+ @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
234
+ oauth_application_view
235
+ end
236
+ end
237
+ request.get do
238
+ oauth_applications_view
239
+ end
240
+ request.post do
241
+ catch_error do
242
+ validate_oauth_application_params
243
+
244
+ transaction do
245
+ before_create_oauth_application
246
+ id = create_oauth_application
247
+ after_create_oauth_application
248
+ set_notice_flash create_oauth_application_notice_flash
249
+ redirect oauth_application_redirect(id)
250
+ end
251
+ end
252
+ set_error_flash create_oauth_application_error_flash
253
+ new_oauth_application_view
254
+ end
255
+ end
256
+ end
257
+
258
+ private
259
+
260
+ def oauth_unique_id_generator
261
+ SecureRandom.uuid
262
+ end
263
+
264
+ def require_oauth_application_account
265
+ throw_json_response_error(authorization_required_error_status, "invalid_client") unless logged_in?
266
+ end
267
+
268
+ # Oauth Application
269
+
270
+ def oauth_application_params
271
+ @oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
272
+ value = request.params[__send__(:"oauth_application_#{param}_param")]
273
+ if value && !value.empty?
274
+ params[param] = value
275
+ else
276
+ set_field_error(param, null_error_message)
277
+ end
278
+ end
279
+ end
280
+
281
+ def validate_oauth_application_params
282
+ oauth_application_params.each do |key, value|
283
+ if key == oauth_application_homepage_url_param ||
284
+ key == oauth_application_redirect_uri_param
285
+
286
+ set_field_error(key, invalid_url_message) unless URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(value)
287
+
288
+ elsif key == oauth_application_scopes_param
289
+
290
+ value.each do |scope|
291
+ set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
292
+ end
293
+ end
294
+ end
295
+
296
+ throw :rodauth_error if @field_errors && !@field_errors.empty?
297
+ end
298
+
299
+ def create_oauth_application
300
+ create_params = {
301
+ oauth_applications_account_id_column => account_id,
302
+ oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
303
+ oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
304
+ oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
305
+ oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param],
306
+ oauth_applications_redirect_uri_column => oauth_application_params[oauth_application_redirect_uri_param]
307
+ }
308
+
309
+ # set client ID/secret pairs
310
+ create_params.merge! \
311
+ oauth_applications_client_id_column => oauth_unique_id_generator,
312
+ oauth_applications_client_secret_column => oauth_unique_id_generator
313
+
314
+ create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
315
+ create_params[oauth_applications_scopes_column].join(",")
316
+ else
317
+ oauth_application_default_scope
318
+ end
319
+
320
+ ds = db[oauth_applications_table]
321
+
322
+ id = nil
323
+ raised = begin
324
+ id = if ds.supports_returning?(:insert)
325
+ ds.returning(oauth_applications_id_column).insert(create_params)
326
+ else
327
+ id = db[oauth_applications_table].insert(create_params)
328
+ db[oauth_applications_table].where(oauth_applications_id_column => id).get(oauth_applications_id_column)
329
+ end
330
+ false
331
+ rescue Sequel::ConstraintViolation => e
332
+ e
333
+ end
334
+
335
+ if raised
336
+ field = raised.message[/\.(.*)$/, 1]
337
+ case raised
338
+ when Sequel::UniqueConstraintViolation
339
+ throw_error(field, unique_error_message)
340
+ when Sequel::NotNullConstraintViolation
341
+ throw_error(field, null_error_message)
342
+ end
343
+ end
344
+
345
+ !raised && id
346
+ end
347
+
348
+ # Authorize
349
+
350
+ def validate_oauth_grant_params
351
+ redirect_response_error("invalid_request") unless oauth_application && check_valid_redirect_uri?
352
+ redirect_response_error("invalid_scope") unless check_valid_scopes?
353
+ end
354
+
355
+ def create_oauth_grant
356
+ create_params = {
357
+ oauth_grants_account_id_column => account_id,
358
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
359
+ oauth_grants_redirect_uri_column => redirect_uri,
360
+ oauth_grants_code_column => oauth_unique_id_generator,
361
+ oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
362
+ oauth_grants_scopes_column => scopes
363
+ }
364
+
365
+ ds = db[oauth_grants_table]
366
+
367
+ begin
368
+ if ds.supports_returning?(:insert)
369
+ ds.returning(authorize_code_column).insert(create_params)
370
+ else
371
+ id = ds.insert(create_params)
372
+ ds.where(oauth_grants_id_column => id).get(oauth_grants_code_column)
373
+ end
374
+ rescue Sequel::UniqueConstraintViolation
375
+ retry
376
+ end
377
+ end
378
+
379
+ # Access Tokens
380
+
381
+ def validate_oauth_token_params
382
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request") unless param(client_id_param)
383
+
384
+ unless (grant_type = param(grant_type_param))
385
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
386
+ end
387
+
388
+ case grant_type
389
+ when "authorization_code"
390
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request") unless param(code_param)
391
+
392
+ when "refresh_token"
393
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request") unless param(refresh_token_param)
394
+ else
395
+ throw_json_response_error(invalid_oauth_response_status, "invalid_request")
396
+ end
397
+ end
398
+
399
+ def create_oauth_token
400
+ case param(grant_type_param)
401
+ when "authorization_code"
402
+ # fetch oauth grant
403
+ oauth_grant = db[oauth_grants_table].where(
404
+ oauth_grants_code_column => param(code_param),
405
+ oauth_grants_redirect_uri_column => param(redirect_uri_param),
406
+ oauth_grants_oauth_application_id_column => db[oauth_applications_table].where(
407
+ oauth_applications_client_id_column => param(client_id_param),
408
+ oauth_applications_account_id_column => oauth_applications_account_id_column
409
+ ).select(oauth_applications_id_column)
410
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
411
+ .where(oauth_grants_revoked_at_column => nil)
412
+ .first
413
+
414
+ throw_json_response_error(invalid_oauth_response_status, "invalid_grant") unless oauth_grant
415
+
416
+ create_params = {
417
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
418
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
419
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column],
420
+ oauth_grants_expires_in_column => Time.now + oauth_token_expires_in,
421
+ oauth_tokens_refresh_token_column => oauth_unique_id_generator,
422
+ oauth_tokens_token_column => oauth_unique_id_generator
423
+ }
424
+
425
+ # revoke oauth grant
426
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
427
+ .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
428
+
429
+ ds = db[oauth_tokens_table]
430
+
431
+ begin
432
+ if ds.supports_returning?(:insert)
433
+ ds.returning.insert(create_params)
434
+ else
435
+ id = ds.insert(create_params)
436
+ ds.where(oauth_tokens_id_column => id).first
437
+ end
438
+ rescue Sequel::UniqueConstraintViolation
439
+ retry
440
+ end
441
+ when "refresh_token"
442
+ # fetch oauth grant
443
+ oauth_token = db[oauth_tokens_table].where(
444
+ oauth_tokens_refresh_token_column => param(refresh_token_param),
445
+ oauth_tokens_oauth_application_id_column => db[oauth_applications_table].where(
446
+ oauth_applications_client_id_column => param(client_id_param),
447
+ oauth_applications_account_id_column => account_id
448
+ ).select(oauth_applications_id_column)
449
+ ).where(oauth_grants_revoked_at_column => nil)
450
+ .first
451
+
452
+ throw_json_response_error(invalid_oauth_response_status, "invalid_grant") unless oauth_token
453
+
454
+ update_params = {
455
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
456
+ oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in,
457
+ oauth_tokens_token_column => oauth_unique_id_generator
458
+ }
459
+
460
+ ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
461
+ begin
462
+ if ds.supports_returning?(:update)
463
+ ds.returning.update(update_params)
464
+ else
465
+ ds.update(update_params)
466
+ ds.first
467
+ end
468
+ rescue Sequel::UniqueConstraintViolation
469
+ retry
470
+ end
471
+ else
472
+ throw_json_response_error(invalid_grant_status, "invalid_grant")
473
+ end
474
+ end
475
+
476
+ def redirect_response_error(error_code)
477
+ redirect_url = URI.parse(request.referer || default_redirect)
478
+ query_params = ["error=#{error_code}"]
479
+ query_params << redirect_url.query if redirect_url.query
480
+ redirect_url.query = query_params.join("&")
481
+ redirect(redirect_url.to_s)
482
+ end
483
+
484
+ def throw_json_response_error(status, error_code)
485
+ response.status = status
486
+ payload = { "error" => error_code }
487
+ payload["error_description"] = send(:"#{error_code}_message") if respond_to?(:"#{error_code}_message")
488
+ response["Content-Type"] ||= json_response_content_type
489
+ response["WWW-Authenticate"] = "Bearer" if status == 401
490
+ response.write(request.send(:convert_to_json, payload))
491
+ request.halt
492
+ end
493
+
494
+ def authorization_required
495
+ if json_request?
496
+ throw_json_response_error(authorization_required_error_status, "invalid_client")
497
+ else
498
+ set_redirect_error_flash(require_authorization_error_flash)
499
+ redirect(require_authorization_redirect)
500
+ end
501
+ end
502
+
503
+ def check_valid_scopes?
504
+ return false unless scopes
505
+
506
+ (scopes.split(",") - oauth_application[oauth_applications_scopes_column].split(",")).empty?
507
+ end
508
+
509
+ def check_valid_redirect_uri?
510
+ redirect_uri == oauth_application[oauth_applications_redirect_uri_column]
511
+ end
512
+
513
+ route(:oauth_token) do |r|
514
+ require_oauth_application_account
515
+
516
+ # access-token
517
+ r.post do
518
+ catch_error do
519
+ validate_oauth_token_params
520
+
521
+ oauth_token = nil
522
+ transaction do
523
+ before_token
524
+ oauth_token = create_oauth_token
525
+ after_token
526
+ end
527
+
528
+ response.status = 200
529
+ response["Content-Type"] ||= json_response_content_type
530
+ json_response = {
531
+ "token" => oauth_token[:token],
532
+ "token_type" => oauth_token_type,
533
+ "refresh_token" => oauth_token[:refresh_token],
534
+ "expires_in" => oauth_token_expires_in
535
+ }
536
+ response.write(request.__send__(:convert_to_json, json_response))
537
+ request.halt
538
+ end
539
+
540
+ throw_json_response_error(json_response_content_type, "invalid_request")
541
+ end
542
+ end
543
+
544
+ route(:oauth_authorize) do |r|
545
+ require_account
546
+
547
+ r.get do
548
+ validate_oauth_grant_params
549
+ authorize_view
550
+ end
551
+
552
+ r.post do
553
+ validate_oauth_grant_params
554
+
555
+ code = nil
556
+ transaction do
557
+ before_authorize
558
+ code = create_oauth_grant
559
+ after_authorize
560
+ end
561
+
562
+ redirect_url = URI.parse(redirect_uri)
563
+ query_params = ["code=#{code}"]
564
+ query_params << "state=#{state}" if state
565
+ query_params << redirect_url.query if redirect_url.query
566
+ redirect_url.query = query_params.join("&")
567
+
568
+ redirect(redirect_url.to_s)
569
+ end
570
+ end
571
+ end
572
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth"
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roda-oauth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tiago Cardoso
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Implementation of the OAuth 2.0 protocol on top of rodauth.
14
+ email:
15
+ - cardoso_tiago@hotmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README.md
20
+ - CHANGELOG.md
21
+ files:
22
+ - CHANGELOG.md
23
+ - README.md
24
+ - lib/rodauth/features/oauth.rb
25
+ - lib/rodauth/oauth.rb
26
+ homepage: https://gitlab.com/honeyryderchuck/roda-oauth
27
+ licenses: []
28
+ metadata:
29
+ homepage_uri: https://gitlab.com/honeyryderchuck/roda-oauth
30
+ source_code_uri: https://gitlab.com/honeyryderchuck/roda-oauth
31
+ changelog_uri: https://gitlab.com/honeyryderchuck/roda-oauth/-/blob/master/CHANGELOG.md
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.1.2
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
51
+ test_files: []