rodauth-oauth 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +333 -0
- data/lib/generators/roda/oauth/install_generator.rb +47 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_application.rb +4 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_grant.rb +4 -0
- data/lib/generators/roda/oauth/templates/app/models/oauth_token.rb +4 -0
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +47 -0
- data/lib/generators/roda/oauth/views_generator.rb +54 -0
- data/lib/rodauth/features/oauth.rb +802 -0
- data/lib/rodauth/oauth.rb +7 -0
- data/lib/rodauth/oauth/railtie.rb +8 -0
- data/lib/rodauth/oauth/version.rb +7 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -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,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
|