rodauth-oauth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cea0f9a4896e57d535c2ef73eff0c1a9e6b4e25f4d53740c65171b8c098a8ac1
4
+ data.tar.gz: 96f78feb4157d6f940700fe658b7817b95bd79b61a6f02b9cc530947512a8ff0
5
+ SHA512:
6
+ metadata.gz: 3b2bc30f8793a0ae0ef8a48b46fcd896494d6a941e57e66481d8b8197424c5a6da80761237e26b4eb70acf73ae933be40484d7ca06cf5191790ee83b62eb7411
7
+ data.tar.gz: 7a1cb3061c3eb9c271b04c35e970306a96018a40408c63c5fd5c815d62f3b4bfdf8c84f32c6947015c8a99a810a8cc904a1f6e10ca3f9eba842ec8575f975a11
@@ -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,333 @@
1
+ # Rodauth::Oauth
2
+
3
+ [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
4
+ [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
5
+
6
+ This is an extension to the `rodauth` gem which adds support for the [OAuth 2.0 protocol](https://tools.ietf.org/html/rfc6749).
7
+
8
+ ## Features
9
+
10
+ This gem implements:
11
+
12
+ * [The OAuth 2.0 protocol framework](https://tools.ietf.org/html/rfc6749):
13
+ * [Authorization grant flow](https://tools.ietf.org/html/rfc6749#section-1.3);
14
+ * [Access Token generation](https://tools.ietf.org/html/rfc6749#section-1.4);
15
+ * [Access Token refresh](https://tools.ietf.org/html/rfc6749#section-1.5);
16
+ * [Token revocation](https://tools.ietf.org/html/rfc7009);
17
+ * [Implicit grant (off by default)[https://tools.ietf.org/html/rfc6749#section-4.2];
18
+ * Access Type (Token refresh online and offline);
19
+ * OAuth application and token management dashboards;
20
+
21
+
22
+ This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
23
+
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem 'rodauth-oauth'
31
+ ```
32
+
33
+ And then execute:
34
+
35
+ $ bundle install
36
+
37
+ Or install it yourself as:
38
+
39
+ $ gem install rodauth-oauth
40
+
41
+ ## Usage
42
+
43
+ This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
44
+
45
+ ```ruby
46
+ plugin :rodauth do
47
+ # enable it in the plugin
48
+ enable :login, :oauth
49
+ oauth_application_default_scope %w[profile.read]
50
+ oauth_application_scopes %w[profile.read profile.write]
51
+ end
52
+
53
+ # then, inside roda
54
+
55
+ route do |r|
56
+ r.rodauth
57
+
58
+ # public routes go here
59
+ # ...
60
+ # here you do your thing
61
+ # authenticated section is here
62
+
63
+ rodauth.require_authentication
64
+
65
+ # oauth will only kick in on ce you call #require_oauth_authorization
66
+
67
+ r.is "users" do
68
+ rodauth.require_oauth_authorization # defaults to profile.read
69
+ r.post do
70
+ rodauth.require_oauth_authorization("profile.write")
71
+ end
72
+ # ...
73
+ end
74
+
75
+ r.is "books" do
76
+ rodauth.require_oauth_authorization("books.read", "books.research")
77
+ r.get do
78
+ # ...
79
+ end
80
+ end
81
+ end
82
+ ```
83
+
84
+ You'll have to do a bit more boilerplate, so here's the instructions.
85
+
86
+ ### Example (TL;DR)
87
+
88
+ If you're familiar with the technology and want to skip the next paragraphs, just [check our roda example](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/examples/roda).
89
+
90
+
91
+ Generating tokens happens mostly server-to-server, so here's an example using:
92
+
93
+ #### Access Token Generation
94
+
95
+ ##### HTTPX
96
+
97
+ ```ruby
98
+ require "httpx"
99
+ httpx = HTTPX.plugin(:authorization)
100
+ response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
101
+ .post("https://auth_server/oauth-token",json: {
102
+ client_id: ENV["OAUTH_CLIENT_ID"],
103
+ grant_type: "authorization_code",
104
+ code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"
105
+ })
106
+ response.raise_for_status
107
+ payload = JSON.parse(response.to_s)
108
+ puts payload #=> {"token" => "awr23f3h8f9d2h89...", "refresh_token" => "23fkop3kr290kc..." ....
109
+ ```
110
+
111
+ ##### cURL
112
+
113
+ ```
114
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/oauth-token
115
+ ```
116
+
117
+ #### Refresh Token
118
+
119
+ Refreshing expired tokens also happens mostly server-to-server, here's an example:
120
+
121
+ ##### HTTPX
122
+
123
+ ```ruby
124
+ require "httpx"
125
+ httpx = HTTPX.plugin(:authorization)
126
+ response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
127
+ .post("https://auth_server/oauth-token",json: {
128
+ client_id: ENV["OAUTH_CLIENT_ID"],
129
+ grant_type: "refresh_token",
130
+ token: "2r89hfef4j9f90d2j2390jf390g"
131
+ })
132
+ response.raise_for_status
133
+ payload = JSON.parse(response.to_s)
134
+ puts payload #=> {"token" => "awr23f3h8f9d2h89...", "token_type" => "Bearer" ....
135
+ ```
136
+
137
+ ##### cURL
138
+
139
+ ```
140
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-token
141
+ ```
142
+
143
+ #### Revoking tokens
144
+
145
+ Token revocation can be done both by the idenntity owner or the application owner, and can therefore be done either online (browser-based form) or server-to-server. Here's an example using server-to-server:
146
+
147
+ ```ruby
148
+ require "httpx"
149
+ httpx = HTTPX.plugin(:authorization)
150
+ response = httpx.with(headers: { "X-your-auth-scheme" => ENV["SERVER_KEY"] })
151
+ .post("https://auth_server/oauth-revoke",json: {
152
+ client_id: ENV["OAUTH_CLIENT_ID"],
153
+ token_type_hint: "access_token", # can also be "refresh:tokn"
154
+ token: "2r89hfef4j9f90d2j2390jf390g"
155
+ })
156
+ response.raise_for_status
157
+ payload = JSON.parse(response.to_s)
158
+ puts payload #=> {"token" => "awr23f3h8f9d2h89...", "token_type" => "Bearer" ....
159
+ ```
160
+
161
+ ##### cURL
162
+
163
+ ```
164
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
165
+ ```
166
+
167
+ ### Database migrations
168
+
169
+ 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/rodauth-oauth/-/tree/master/test/migrate) (omit the first 2 if you already have account tables).
170
+
171
+ 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:
172
+
173
+ ```ruby
174
+ plugin :rodauth do
175
+ # enable it in the plugin
176
+ enable :login, :oauth
177
+ # ...
178
+ oauth_grants_table "access_grants"
179
+ oauth_grants_code_column "authorization_code"
180
+ end
181
+ ```
182
+
183
+ If you're starting from scratch though, the recommendation is to stick to the defaults.
184
+
185
+ ### HTML views
186
+
187
+ You'll have to generate HTML templates for the Oauth Authorization form.
188
+
189
+ 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).
190
+
191
+ ### Endpoints
192
+
193
+ Once you set it up, by default, the following endpoints will be available:
194
+
195
+ * `GET /oauth-authorize`: Loads the OAuth authorization HTML form;
196
+ * `POST /oauth-authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
197
+ * `POST /oauth-token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
198
+ * `POST /oauth-revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
199
+
200
+ ### OAuth applications
201
+
202
+ 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:
203
+
204
+ ```ruby
205
+ route do |r|
206
+ r.rodauth
207
+ # don't forget to authenticate to access the dashboard
208
+ rodauth.require_authentication
209
+ rodauth.oauth_applications
210
+ # ...
211
+ end
212
+ ```
213
+
214
+ This will define the following endpoints:
215
+
216
+ * `GET /oauth-applications`: returns the OAuth applications HTML dashboard;
217
+ * `GET /oauth-applications/{application_id}`: returns an OAuth application HTML page;
218
+ * `GET /oauth-applications/{application_id}/oauth-tokens`: returns the OAuth tokens from an OAuth application HTML page;
219
+ * `GET /oauth-applications/new`: returns a new OAuth application form;
220
+ * `POST /oauth-applications`: processes a new OAuth application request;
221
+
222
+ As in the OAuth authorization form example, you'll have to define the following HTML templates in order to use this feature:
223
+
224
+ * `oauth_applications.(erb|str|...)`: the list of OAuth applications;
225
+ * `oauth_application.(erb|str|...)`: the OAuth application page;
226
+ * `new_oauth_application.(erb|str|...)`: the new OAuth application form;
227
+ * `oauth_tokens.(erb|str|...)`: the list of OAuth tokens from an application;
228
+
229
+ ## Rails
230
+
231
+ This library provides a thin integration layer on top of [rodauth-rails](https://github.com/janko/rodauth-rails). Therefore, the first step you'll have to take is to integrate it in your project. Fortunately, it's very straightforward.
232
+
233
+ You'll have to run the generator task to create the necessary migrations and views:
234
+
235
+ ```
236
+ > bundle exec rails generate rodauth:oauth:install
237
+ # create a migration file, db/migrate(*_create_rodauth_oauth.rb);
238
+ # Oauth Application, Grant and Token models into app/models;
239
+ > bundle exec rails generate rodauth:oauth:views
240
+ # creates view files under app/views/rodauth
241
+ ```
242
+
243
+ You are encouraged to check the output and adapt it to your needs.
244
+
245
+ You can then enable this feature in `lib/rodauth_app.rb` and set up any options you want:
246
+
247
+ ```ruby
248
+ # lib/roudauth_app.rb
249
+ enable :oauth
250
+ # OAuth
251
+ oauth_application_default_scope "profile.read"
252
+ oauth_application_scopes %w[profile.read profile.write books.read books.write]
253
+ ```
254
+
255
+ Now that you're set up, you can use the `rodauth` object to deny access to certain subsets of your app/API:
256
+
257
+ ```ruby
258
+ class BooksController < ApplicationController
259
+ before_action :allow_read_access, only: %i[index show]
260
+ before_action :allow_write_access, only: %i[create update]
261
+
262
+ def index
263
+ # ...
264
+ end
265
+
266
+ def show
267
+ # ...
268
+ end
269
+
270
+ def create
271
+ # ...
272
+ end
273
+
274
+ def update
275
+ # ...
276
+ end
277
+
278
+ private
279
+
280
+ def allow_read_access
281
+ rodauth.require_oauth_authorization("books.read")
282
+ end
283
+
284
+ def allow_write_access
285
+ rodauth.require_oauth_authorization("books.write")
286
+ end
287
+ end
288
+ ```
289
+
290
+ ## Features
291
+
292
+ In this section, the non-standard features are going to be described in more detail.
293
+
294
+ ### Access Type (default: "offline")
295
+
296
+ The "access_type" feature allows the authorization server to emit access tokens with no associated refresh token. This means that users with expired access tokens will have to go through the OAuth flow everytime they need a new one.
297
+
298
+ In order to enable this option, add "access_type=online" to the query params section of the authorization url.
299
+
300
+ **Note**: this feature does not yet support the "approval_prompt" feature.
301
+
302
+
303
+ ### Implicit Grant (default: disabled)
304
+
305
+ The implicit grant flow is part of the original OAuth 2.0 RFC, however, if you care about security, you are **strongly recommended** not to enable it.
306
+
307
+ However, if you really need it, just pass the option when enabling the `rodauth` plugin:
308
+
309
+ ```ruby
310
+ plugin :rodauth do
311
+ enable :oauth
312
+ use_oauth_implicit_grant_type true
313
+ end
314
+ ```
315
+
316
+ And add "response_type=token" to the query params section of the authorization url.
317
+
318
+ ## Ruby support policy
319
+
320
+ The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support.
321
+
322
+ ### JRuby
323
+
324
+ If you're interested in using this library in rails, be warned that `rodauth-rails` doesn't support it yet (although, this is expected to change at some point).
325
+
326
+ ## Development
327
+
328
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests, and `rake rubocop` to run the linter.
329
+
330
+ ## Contributing
331
+
332
+ Bug reports and pull requests are welcome on Gitlab at https://gitlab.com/honeyryderchuck/rodauth-oauth.
333
+
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/migration"
5
+ require "rails/generators/active_record"
6
+
7
+ module Rodauth::OAuth::Rails
8
+ module Generators
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ include ::Rails::Generators::Migration
11
+
12
+ source_root "#{__dir__}/templates"
13
+ namespace "roda:oauth:install"
14
+
15
+ def create_rodauth_migration
16
+ return unless defined?(ActiveRecord::Base)
17
+
18
+ migration_template "db/migrate/create_rodauth_oauth.rb", "db/migrate/create_rodauth_oauth.rb"
19
+ end
20
+
21
+ def create_oauth_models
22
+ return unless defined?(ActiveRecord::Base)
23
+
24
+ template "app/models/oauth_application.rb"
25
+ template "app/models/oauth_grant.rb"
26
+ template "app/models/oauth_token.rb"
27
+ end
28
+
29
+ private
30
+
31
+ # required by #migration_template action
32
+ def self.next_migration_number(dirname)
33
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
34
+ end
35
+
36
+ def migration_version
37
+ if ActiveRecord.version >= Gem::Version.new("5.0.0")
38
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
39
+ end
40
+ end
41
+
42
+ def adapter
43
+ ActiveRecord::Base.connection_config.fetch(:adapter)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OauthApplication < ApplicationRecord
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OauthGrant < ApplicationRecord
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OauthToken < ApplicationRecord
4
+ end
@@ -0,0 +1,47 @@
1
+ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :oauth_applications do |t|
4
+ t.integer :account_id
5
+ t.foreign_key :accounts, column: :account_id
6
+ t.string :name, null: false
7
+ t.string :description, null: false
8
+ t.string :homepage_url, null: false
9
+ t.string :redirect_uri, null: false
10
+ t.string :client_id, null: false, index: { unique: true }
11
+ t.string :client_secret, null: false, index: { unique: true }
12
+ t.string :scopes, null: false
13
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
14
+ end
15
+
16
+ create_table :oauth_grants do |t|
17
+ t.integer :account_id
18
+ t.foreign_key :accounts, column: :account_id
19
+ t.integer :oauth_application_id
20
+ t.foreign_key :oauth_applications, column: :oauth_application_id
21
+ t.string :code, null: false
22
+ t.datetime :expires_in, null: false
23
+ t.string :redirect_uri
24
+ t.datetime :revoked_at
25
+ t.string :scopes, null: false
26
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
27
+ t.index(%i[oauth_application_id code], unique: true)
28
+ end
29
+
30
+ create_table :oauth_tokens do |t|
31
+ t.integer :account_id
32
+ t.foreign_key :accounts, column: :account_id
33
+ t.integer :oauth_grant_id
34
+ t.foreign_key :oauth_grants, column: :oauth_grant_id
35
+ t.integer :oauth_token_id
36
+ t.foreign_key :oauth_tokens, column: :oauth_token_id
37
+ t.integer :oauth_application_id
38
+ t.foreign_key :oauth_applications, column: :oauth_application_id
39
+ t.string :token, null: false, token: true
40
+ t.string :refresh_token
41
+ t.datetime :expires_in, null: false
42
+ t.datetime :revoked_at
43
+ t.string :scopes, null: false
44
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
45
+ end
46
+ end
47
+ end