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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +137 -0
- data/lib/rodauth/features/oauth.rb +572 -0
- data/lib/rodauth/oauth.rb +3 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -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
|
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: []
|