rodauth-omniauth 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +364 -0
- data/lib/rodauth/features/omniauth.rb +195 -0
- data/lib/rodauth/features/omniauth_base.rb +213 -0
- data/rodauth-omniauth.gemspec +31 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 99f2cd5c47082f18ff2b0feeea0106c104edf6454e354fb04f8c74044b7796b2
|
4
|
+
data.tar.gz: 58c096a1176cce8c2aa1ef0741c35e0ec81a36fb1d3d7bf9350b01c143daffdb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ee9f500a18535a215d74d6cbc250561a7d0b61cc5818166e7bf481b74bf67dedf2547228ffbec177273715d1001cce2178e7590cadc8c21b102ef1b9585f9c5
|
7
|
+
data.tar.gz: fe0337fdc2ef82ea53c5b8514383df10d60433900deaa35fa519ffe0e846c3a5e79deead46b1e790e29a35ffac0a00a2301c6437376d8fd91832d8ee1cb49a8e
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Janko Marohnić
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,364 @@
|
|
1
|
+
# rodauth-omniauth
|
2
|
+
|
3
|
+
[Rodauth] feature that offers login and registration via multiple external providers using [OmniAuth]. The external identities are automatically stored in the database, and associated to the main account record.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add the gem to your project:
|
8
|
+
|
9
|
+
```sh
|
10
|
+
$ bundle add rodauth-omniauth
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
You'll first need to create the table for storing external identities:
|
16
|
+
|
17
|
+
```rb
|
18
|
+
Sequel.migration do # class CreateAccountIdentities < ActiveRecord::Migration
|
19
|
+
change do # def change
|
20
|
+
create_table :account_identities do # create_table :account_identities do |t|
|
21
|
+
primary_key :id # t.references :account, null: false, foreign_key: { on_delete: :cascade }
|
22
|
+
foreign_key :account_id, :accounts # t.string :provider, null: false
|
23
|
+
String :provider, null: false # t.string :uid, null: false
|
24
|
+
String :uid, null: false # t.index [:provider, :uid], unique: true
|
25
|
+
unique [:provider, :uid] # end
|
26
|
+
end # end
|
27
|
+
end # end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
Then enable the `omniauth` feature and register providers in your Rodauth configuration:
|
32
|
+
|
33
|
+
```sh
|
34
|
+
$ bundle add omniauth-facebook omniauth-twitter, omniauth-google_oauth2
|
35
|
+
```
|
36
|
+
```rb
|
37
|
+
plugin :rodauth do
|
38
|
+
enable :omniauth
|
39
|
+
|
40
|
+
omniauth_provider :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], scope: "email"
|
41
|
+
omniauth_provider :twitter, ENV["TWITTER_API_KEY"], ENV["TWITTER_API_SECRET"]
|
42
|
+
omniauth_provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"], name: :google
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
You can now add authentication links to your login form:
|
47
|
+
|
48
|
+
```erb
|
49
|
+
<!-- app/views/rodauth/_login_form_footer.html.erb -->
|
50
|
+
<%== rodauth.login_form_footer_links_heading %>
|
51
|
+
|
52
|
+
<ul>
|
53
|
+
<li><%= button_to "Login via Facebook", rodauth.omniauth_request_path(:facebook), method: :post, data: { turbo: false }, class: "btn btn-link p-0" %></li>
|
54
|
+
<li><%= button_to "Login via Twitter", rodauth.omniauth_request_path(:twitter), method: :post, data: { turbo: false }, class: "btn btn-link p-0" %></li>
|
55
|
+
<li><%= button_to "Login via Google", rodauth.omniauth_request_path(:google), method: :post, data: { turbo: false }, class: "btn btn-link p-0" %></li>
|
56
|
+
<% rodauth.login_form_footer_links.each do |_, link, text| %>
|
57
|
+
<li><%= link_to text, link %></li>
|
58
|
+
<% end %>
|
59
|
+
</ul>
|
60
|
+
```
|
61
|
+
|
62
|
+
Assuming you configured the providers correctly, you should now be able to authenticate via an external provider. The `omniauth` feature handles the callback request, automatically creating new identities and verified accounts from those identities as needed.
|
63
|
+
|
64
|
+
```rb
|
65
|
+
DB[:accounts].all
|
66
|
+
#=> [{ id: 123, status_id: 2, email: "user@example.com" }]
|
67
|
+
DB[:account_identities].all
|
68
|
+
#=> [{ id: 456, account_id: 123, provider: "facebook", uid: "984346198764" },
|
69
|
+
# { id: 789, account_id: 123, provider: "google", uid: "5871623487134"}]
|
70
|
+
```
|
71
|
+
|
72
|
+
Currently, provider login is required to return the user's email address, and account creation is assumed not to require additional fields that need to be entered manually. There is currently also no built-in functionality for connecting/removing external identities when signed in. Both features are planned for future versions.
|
73
|
+
|
74
|
+
### Login
|
75
|
+
|
76
|
+
If the local account associated to the external identity exists and is unverified (e.g. it was created through normal registration), the external login will abort during the callback phase. You can change the default error flash and redirect location in this case:
|
77
|
+
|
78
|
+
```rb
|
79
|
+
omniauth_login_unverified_account_error_flash "The account matching the external identity is currently awaiting verification"
|
80
|
+
omniauth_login_failure_redirect { require_login_redirect }
|
81
|
+
```
|
82
|
+
|
83
|
+
### Account creation
|
84
|
+
|
85
|
+
Since provider accounts have verified the email address, local accounts created via external logins are automatically considered verified.
|
86
|
+
|
87
|
+
If you want to use extra user information for account creation, you can do so via hooks:
|
88
|
+
|
89
|
+
```rb
|
90
|
+
before_omniauth_create_account { account[:name] = omniauth_name }
|
91
|
+
# or
|
92
|
+
after_omniauth_create_account do
|
93
|
+
Profile.create(account_id: account_id, bio: omniauth_info["description"], image_url: omniauth_info["image"])
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
When the account is closed, its external identities are automatically cleared from the database.
|
98
|
+
|
99
|
+
### Identity data
|
100
|
+
|
101
|
+
You can also store extra data on the external identities. For example, we could override the update hash to store `info`, `credentials`, and `extra` data from the auth hash into separate columns:
|
102
|
+
|
103
|
+
```rb
|
104
|
+
alter_table :account_identities do
|
105
|
+
add_column :info, :json, default: "{}"
|
106
|
+
add_column :credentials, :json, default: "{}"
|
107
|
+
add_column :extra, :json, default: "{}"
|
108
|
+
end
|
109
|
+
```
|
110
|
+
```rb
|
111
|
+
# this data will be refreshed on each login
|
112
|
+
omniauth_identity_update_hash do
|
113
|
+
{
|
114
|
+
info: omniauth_info.to_json,
|
115
|
+
credentials: omniauth_credentials.to_json,
|
116
|
+
extra: omniauth_extra.to_json,
|
117
|
+
}
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
With this configuration, the identity record will be automatically synced with most recent state on each provider login. If you would like to only save provider data on first login, you can override the insert hash instead:
|
122
|
+
|
123
|
+
```rb
|
124
|
+
# this data will be stored only on first login
|
125
|
+
omniauth_identity_insert_hash do
|
126
|
+
super().merge(
|
127
|
+
info: omniauth_info.to_json,
|
128
|
+
credentials: omniauth_credentials.to_json,
|
129
|
+
extra: omniauth_extra.to_json,
|
130
|
+
}
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
### Model associations
|
135
|
+
|
136
|
+
When using the [rodauth-model] gem, an `identities` one-to-many association will be defined on the account model:
|
137
|
+
|
138
|
+
```rb
|
139
|
+
require "rodauth/model"
|
140
|
+
|
141
|
+
class Account < Sequel::Model
|
142
|
+
include Rodauth::Model(RodauthApp.rodauth)
|
143
|
+
end
|
144
|
+
```
|
145
|
+
```rb
|
146
|
+
Account.first.identities #=>
|
147
|
+
# [
|
148
|
+
# #<Account::Identity id=123 provider="facebook" uid="987434628">,
|
149
|
+
# #<Account::Identity id=456 provider="google" uid="274673644">
|
150
|
+
# ]
|
151
|
+
```
|
152
|
+
|
153
|
+
## Base
|
154
|
+
|
155
|
+
The `omniauth` feature builds on top of the `omniauth_base` feature, which sets up OmniAuth and routes its requests, but has no interaction with the database. So, if you would prefer to handle external logins differently, you can load just the `omniauth_base` feature, and implement your own callbacks.
|
156
|
+
|
157
|
+
```rb
|
158
|
+
plugin :rodauth do
|
159
|
+
enable :omniauth_base
|
160
|
+
|
161
|
+
omniauth_provider :github, ENV["GITHUB_KEY"], ENV["GITHUB_SECRET"], scope: "user"
|
162
|
+
omniauth_provider :apple, ENV["CLIENT_ID"], { scope: "email name", ... }
|
163
|
+
end
|
164
|
+
|
165
|
+
route do |r|
|
166
|
+
r.rodauth # routes Rodauth and OmniAuth requests
|
167
|
+
|
168
|
+
r.get "auth", String, "callback" do
|
169
|
+
# ... handle callback request ...
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
### Helpers
|
175
|
+
|
176
|
+
There are various helper methods available for reading OmniAuth data:
|
177
|
+
|
178
|
+
```rb
|
179
|
+
# retrieving the auth hash:
|
180
|
+
rodauth.omniauth_auth #=> { "provider" => "twitter", "uid" => "49823724", "info" => { "email" => "user@example.com", "name" => "John Smith", ... }, ... }
|
181
|
+
rodauth.omniauth_provider #=> "twitter"
|
182
|
+
rodauth.omniauth_uid #=> "49823724"
|
183
|
+
rodauth.omniauth_info #=> { "email" => "user@example.com", "name" => "John Smith", ... }
|
184
|
+
rodauth.omniauth_email #=> "user@example.com"
|
185
|
+
rodauth.omniauth_name #=> "John Smith"
|
186
|
+
rodauth.omniauth_credentials #=> returns "credentials" value from auth hash
|
187
|
+
rodauth.omniauth_extra #=> returns "extra" value from auth hash
|
188
|
+
|
189
|
+
# retrieving additional information:
|
190
|
+
rodauth.omniauth_strategy #=> #<OmniAuth::Strategies::Twitter ...>
|
191
|
+
rodauth.omniauth_params # returns GET params from request phase
|
192
|
+
rodauth.omniauth_origin # returns origin from request phase (usually referrer)
|
193
|
+
|
194
|
+
# retrieving error information in case of a login failure
|
195
|
+
rodauth.omniauth_error # returns the exception object
|
196
|
+
rodauth.omniauth_error_type # returns the error type symbol (strategy-specific)
|
197
|
+
rodauth.omniauth_error_strategy # returns the strategy for which the error occured
|
198
|
+
```
|
199
|
+
|
200
|
+
### URLs
|
201
|
+
|
202
|
+
URL helpers are provided as well:
|
203
|
+
|
204
|
+
```rb
|
205
|
+
rodauth.prefix #=> "/user"
|
206
|
+
rodauth.omniauth_prefix #=> "/auth"
|
207
|
+
|
208
|
+
rodauth.omniauth_request_route #=> "auth/facebook"
|
209
|
+
rodauth.omniauth_request_path #=> "/user/auth/facebook"
|
210
|
+
rodauth.omniauth_request_url #=> "https://example.com/user/auth/facebook"
|
211
|
+
|
212
|
+
rodauth.omniauth_callback_route #=> "auth/facebook/callback"
|
213
|
+
rodauth.omniauth_callback_path #=> "/user/auth/facebook/callback"
|
214
|
+
rodauth.omniauth_callback_url #=> "https://example.com/user/auth/facebook/callback"
|
215
|
+
```
|
216
|
+
|
217
|
+
The prefix for the OmniAuth app can be changed:
|
218
|
+
|
219
|
+
```rb
|
220
|
+
omniauth_prefix "/external"
|
221
|
+
```
|
222
|
+
|
223
|
+
### Hooks
|
224
|
+
|
225
|
+
OmniAuth configuration has global hooks for various phases, which get called with the Rack env hash. Here you can use corresponding Rodauth configuration methods, which are executed in Rodauth context:
|
226
|
+
|
227
|
+
```rb
|
228
|
+
omniauth_setup { ... }
|
229
|
+
omniauth_request_validation_phase { ... }
|
230
|
+
omniauth_before_request_phase { ... }
|
231
|
+
omniauth_before_callback_phase { ... }
|
232
|
+
omniauth_on_failure { ... }
|
233
|
+
```
|
234
|
+
|
235
|
+
You can use the `omniauth_strategy` helper method to differentiate between strategies:
|
236
|
+
|
237
|
+
```rb
|
238
|
+
omniauth_setup do
|
239
|
+
if omniauth_strategy.name == :github
|
240
|
+
omniauth_strategy.options[:foo] = "bar"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
#### Failure
|
246
|
+
|
247
|
+
The default reaction to login failure is to redirect to the root page with an error flash message. You can change the configuration:
|
248
|
+
|
249
|
+
```rb
|
250
|
+
omniauth_failure_error_flash "There was an error logging in with the external provider"
|
251
|
+
omniauth_failure_redirect { default_redirect }
|
252
|
+
omniauth_failure_error_status 500 # for JSON API
|
253
|
+
```
|
254
|
+
|
255
|
+
Or provide your own implementation:
|
256
|
+
|
257
|
+
```rb
|
258
|
+
omniauth_on_failure do
|
259
|
+
case omniauth_error_type
|
260
|
+
when :no_authorization_code then ...
|
261
|
+
when :uknown_signature_algorithm then ...
|
262
|
+
else ...
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
#### CSRF protection
|
268
|
+
|
269
|
+
The default request validation phase uses Rodauth's configured CSRF protection, so there is no need for external gems such as `omniauth-rails_csrf_protection`.
|
270
|
+
|
271
|
+
### Inheritance
|
272
|
+
|
273
|
+
The registered providers are inherited between Rodauth auth classes, so you can have fine-grained configuration for different account types.
|
274
|
+
|
275
|
+
```rb
|
276
|
+
class RodauthBase < Rodauth::Auth
|
277
|
+
configure do
|
278
|
+
enable :omniauth_base
|
279
|
+
omniauth_provider :google_oauth2, ...
|
280
|
+
end
|
281
|
+
end
|
282
|
+
```
|
283
|
+
```rb
|
284
|
+
class RodauthMain < RodauthBase
|
285
|
+
configure do
|
286
|
+
omniauth_provider :facebook, ...
|
287
|
+
end
|
288
|
+
end
|
289
|
+
```
|
290
|
+
```rb
|
291
|
+
class RodauthAdmin < RodauthBase
|
292
|
+
configure do
|
293
|
+
omniauth_provider :twitter, ...
|
294
|
+
omniauth_provider :github, ...
|
295
|
+
end
|
296
|
+
end
|
297
|
+
```
|
298
|
+
```rb
|
299
|
+
class RodauthApp < Roda
|
300
|
+
plugin :rodauth, auth_class: RodauthMain
|
301
|
+
plugin :rodauth, auth_class: RodauthAdmin, name: :admin
|
302
|
+
end
|
303
|
+
```
|
304
|
+
```rb
|
305
|
+
rodauth.omniauth_providers #=> [:google_oauth2, :facebook]
|
306
|
+
rodauth(:admin).omniauth_providers #=> [:google_oauth2, :twitter, :github]
|
307
|
+
```
|
308
|
+
|
309
|
+
### JSON
|
310
|
+
|
311
|
+
JSON requests are supported for the request and callback phases. The request phase endpoint will return the authorize URL:
|
312
|
+
|
313
|
+
```http
|
314
|
+
POST /auth/facebook
|
315
|
+
Accept: application/json
|
316
|
+
Content-Type: application/json
|
317
|
+
|
318
|
+
200 OK
|
319
|
+
Content-Type: application/json
|
320
|
+
{ "authorize_url": "https://external.com/login" }
|
321
|
+
```
|
322
|
+
|
323
|
+
If there was a login failure, the error type will be included in the response:
|
324
|
+
|
325
|
+
```http
|
326
|
+
POST /auth/facebook/callback
|
327
|
+
Accept: application/json
|
328
|
+
Content-Type: application/json
|
329
|
+
|
330
|
+
500 Internal Server Error
|
331
|
+
Content-Type: application/json
|
332
|
+
{ "error_type": "some_error", "error": "There was an error logging in with the external provider" }
|
333
|
+
```
|
334
|
+
|
335
|
+
You can change authorize URL and error type keys:
|
336
|
+
|
337
|
+
```rb
|
338
|
+
omniauth_authorize_url_key "authorize_url"
|
339
|
+
omniauth_error_type_key "error_type"
|
340
|
+
```
|
341
|
+
|
342
|
+
### JWT
|
343
|
+
|
344
|
+
JWT requests are supported for the request and callback phases. OmniAuth information will be stored in JWT session data during the request phase, and restored during the callback phase, as long as the updated JWT token is passed.
|
345
|
+
|
346
|
+
## Development
|
347
|
+
|
348
|
+
Run tests with Rake:
|
349
|
+
|
350
|
+
```sh
|
351
|
+
$ bundle exec rake test
|
352
|
+
```
|
353
|
+
|
354
|
+
## License
|
355
|
+
|
356
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
357
|
+
|
358
|
+
## Code of Conduct
|
359
|
+
|
360
|
+
Everyone interacting in the rodauth-omniauth project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/janko/rodauth-pwned/blob/master/CODE_OF_CONDUCT.md).
|
361
|
+
|
362
|
+
[Rodauth]: https://github.com/jeremyevans/rodauth
|
363
|
+
[OmniAuth]: https://github.com/omniauth/omniauth
|
364
|
+
[rodauth-model]: https://github.com/janko/rodauth-model
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "omniauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:omniauth, :Omniauth) do
|
7
|
+
depends :omniauth_base, :login
|
8
|
+
|
9
|
+
before :omniauth_callback_route
|
10
|
+
before :omniauth_create_account
|
11
|
+
after :omniauth_create_account
|
12
|
+
|
13
|
+
error_flash "The account matching the external identity is currently awaiting verification", :omniauth_login_unverified_account
|
14
|
+
|
15
|
+
redirect(:omniauth_login_failure) { require_login_redirect }
|
16
|
+
|
17
|
+
auth_value_method :omniauth_identities_table, :account_identities
|
18
|
+
auth_value_method :omniauth_identities_id_column, :id
|
19
|
+
auth_value_method :omniauth_identities_account_id_column, :account_id
|
20
|
+
auth_value_method :omniauth_identities_provider_column, :provider
|
21
|
+
auth_value_method :omniauth_identities_uid_column, :uid
|
22
|
+
|
23
|
+
auth_methods(
|
24
|
+
:create_omniauth_identity,
|
25
|
+
:omniauth_identity_insert_hash,
|
26
|
+
:omniauth_identity_update_hash,
|
27
|
+
:remove_omniauth_identities,
|
28
|
+
:update_omniauth_identity,
|
29
|
+
:omniauth_save_account,
|
30
|
+
)
|
31
|
+
|
32
|
+
auth_private_methods(
|
33
|
+
:retrieve_omniauth_identity,
|
34
|
+
:account_from_omniauth_identity,
|
35
|
+
:omniauth_new_account,
|
36
|
+
)
|
37
|
+
|
38
|
+
def route_omniauth!
|
39
|
+
result = super
|
40
|
+
handle_omniauth_callback if omniauth_request?
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_omniauth_callback
|
45
|
+
request.is omniauth_callback_route(omniauth_provider) do
|
46
|
+
_handle_omniauth_callback
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def _handle_omniauth_callback
|
51
|
+
before_omniauth_callback_route
|
52
|
+
|
53
|
+
retrieve_omniauth_identity
|
54
|
+
|
55
|
+
if !account && omniauth_identity
|
56
|
+
account_from_omniauth_identity
|
57
|
+
end
|
58
|
+
|
59
|
+
unless account
|
60
|
+
account_from_login(omniauth_email)
|
61
|
+
end
|
62
|
+
|
63
|
+
if account && !open_account?
|
64
|
+
set_redirect_error_status unopen_account_error_status
|
65
|
+
set_redirect_error_flash omniauth_login_unverified_account_error_flash
|
66
|
+
redirect omniauth_login_failure_redirect
|
67
|
+
end
|
68
|
+
|
69
|
+
transaction do
|
70
|
+
unless account
|
71
|
+
omniauth_new_account
|
72
|
+
before_omniauth_create_account
|
73
|
+
omniauth_save_account
|
74
|
+
after_omniauth_create_account
|
75
|
+
end
|
76
|
+
|
77
|
+
if omniauth_identity
|
78
|
+
update_omniauth_identity
|
79
|
+
else
|
80
|
+
create_omniauth_identity
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
login("omniauth")
|
85
|
+
end
|
86
|
+
|
87
|
+
def retrieve_omniauth_identity
|
88
|
+
@omniauth_identity = _retrieve_omniauth_identity(omniauth_provider, omniauth_uid)
|
89
|
+
end
|
90
|
+
|
91
|
+
def account_from_omniauth_identity
|
92
|
+
@account = _account_from_omniauth_identity
|
93
|
+
end
|
94
|
+
|
95
|
+
def omniauth_new_account
|
96
|
+
@account = _omniauth_new_account(omniauth_email)
|
97
|
+
end
|
98
|
+
|
99
|
+
def omniauth_save_account
|
100
|
+
account[account_id_column] = db[accounts_table].insert(account)
|
101
|
+
end
|
102
|
+
|
103
|
+
def remove_omniauth_identities
|
104
|
+
omniauth_account_identities_ds.delete
|
105
|
+
end
|
106
|
+
|
107
|
+
def possible_authentication_methods
|
108
|
+
methods = super
|
109
|
+
methods << "omniauth" unless methods.include?("password") || omniauth_account_identities_ds.empty?
|
110
|
+
methods
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def allow_email_auth?
|
116
|
+
(defined?(super) ? super : true) && omniauth_account_identities_ds.empty?
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_reader :omniauth_identity
|
120
|
+
|
121
|
+
def _omniauth_new_account(login)
|
122
|
+
acc = { login_column => login }
|
123
|
+
unless skip_status_checks?
|
124
|
+
acc[account_status_column] = account_open_status_value
|
125
|
+
end
|
126
|
+
acc
|
127
|
+
end
|
128
|
+
|
129
|
+
def create_omniauth_identity
|
130
|
+
identity_id = omniauth_identities_ds.insert(omniauth_identity_insert_hash)
|
131
|
+
@omniauth_identity = { omniauth_identities_id_column => identity_id }
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_omniauth_identity(identity_id = omniauth_identity_id)
|
135
|
+
update_hash = omniauth_identity_update_hash
|
136
|
+
return if update_hash.empty?
|
137
|
+
|
138
|
+
omniauth_identities_ds
|
139
|
+
.where(omniauth_identities_id_column => identity_id)
|
140
|
+
.update(update_hash)
|
141
|
+
end
|
142
|
+
|
143
|
+
def omniauth_identity_insert_hash
|
144
|
+
{
|
145
|
+
omniauth_identities_account_id_column => account_id,
|
146
|
+
omniauth_identities_provider_column => omniauth_provider.to_s,
|
147
|
+
omniauth_identities_uid_column => omniauth_uid,
|
148
|
+
}.merge(omniauth_identity_update_hash)
|
149
|
+
end
|
150
|
+
|
151
|
+
def omniauth_identity_update_hash
|
152
|
+
{}
|
153
|
+
end
|
154
|
+
|
155
|
+
def _retrieve_omniauth_identity(provider, uid)
|
156
|
+
omniauth_identities_ds.first(
|
157
|
+
omniauth_identities_provider_column => provider.to_s,
|
158
|
+
omniauth_identities_uid_column => uid,
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def _account_from_omniauth_identity
|
163
|
+
account_ds(omniauth_identity_account_id).first
|
164
|
+
end
|
165
|
+
|
166
|
+
def after_close_account
|
167
|
+
super if defined?(super)
|
168
|
+
remove_omniauth_identities
|
169
|
+
end
|
170
|
+
|
171
|
+
def omniauth_identity_id
|
172
|
+
omniauth_identity[omniauth_identities_id_column]
|
173
|
+
end
|
174
|
+
|
175
|
+
def omniauth_identity_account_id
|
176
|
+
omniauth_identity[omniauth_identities_account_id_column]
|
177
|
+
end
|
178
|
+
|
179
|
+
def omniauth_account_identities_ds(acct_id = nil)
|
180
|
+
acct_id ||= account ? account_id : session_value
|
181
|
+
|
182
|
+
omniauth_identities_ds.where(omniauth_identities_account_id_column => acct_id)
|
183
|
+
end
|
184
|
+
|
185
|
+
def omniauth_identities_ds
|
186
|
+
db[omniauth_identities_table]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if defined?(Rodauth::Model)
|
192
|
+
Rodauth::Model.register_association(:identities) do
|
193
|
+
{ name: :identities, type: :many, table: omniauth_identities_table, key: omniauth_identities_id_column }
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "omniauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:omniauth_base, :OmniauthBase) do
|
7
|
+
error_flash "There was an error logging in with the external provider", :omniauth_failure
|
8
|
+
|
9
|
+
redirect :omniauth_failure
|
10
|
+
|
11
|
+
auth_value_method :omniauth_prefix, OmniAuth.config.path_prefix
|
12
|
+
auth_value_method :omniauth_failure_error_status, 500
|
13
|
+
|
14
|
+
auth_value_method :omniauth_authorize_url_key, "authorize_url"
|
15
|
+
auth_value_method :omniauth_error_type_key, "error_type"
|
16
|
+
|
17
|
+
auth_methods(
|
18
|
+
:build_omniauth_app,
|
19
|
+
:omniauth_before_callback_phase,
|
20
|
+
:omniauth_before_request_phase,
|
21
|
+
:omniauth_on_failure,
|
22
|
+
:omniauth_request_validation_phase,
|
23
|
+
:omniauth_setup,
|
24
|
+
)
|
25
|
+
|
26
|
+
configuration_module_eval do
|
27
|
+
def omniauth_provider(provider, *args)
|
28
|
+
@auth.instance_exec { @omniauth_providers << [provider, *args] }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def post_configure
|
33
|
+
super
|
34
|
+
|
35
|
+
omniauth_app = build_omniauth_app.to_app
|
36
|
+
self.class.send(:define_method, :omniauth_app) { omniauth_app }
|
37
|
+
|
38
|
+
self.class.roda_class.plugin :run_handler
|
39
|
+
end
|
40
|
+
|
41
|
+
def route!
|
42
|
+
super
|
43
|
+
route_omniauth!
|
44
|
+
end
|
45
|
+
|
46
|
+
def route_omniauth!
|
47
|
+
omniauth_run omniauth_app
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
{ request: "", callback: "/callback" }.each do |phase, suffix|
|
52
|
+
define_method(:"omniauth_#{phase}_url") do |provider, params = {}|
|
53
|
+
route_url(send(:"omniauth_#{phase}_route", provider), params)
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method(:"omniauth_#{phase}_path") do |provider, params = {}|
|
57
|
+
route_path(send(:"omniauth_#{phase}_route", provider), params)
|
58
|
+
end
|
59
|
+
|
60
|
+
define_method(:"omniauth_#{phase}_route") do |provider|
|
61
|
+
"#{omniauth_prefix[1..-1]}/#{provider}#{suffix}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
%w[email name].each do |info_key|
|
66
|
+
define_method(:"omniauth_#{info_key}") do
|
67
|
+
omniauth_info[info_key]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
%w[provider uid info credentials extra].each do |auth_key|
|
72
|
+
define_method(:"omniauth_#{auth_key}") do
|
73
|
+
omniauth_auth.fetch(auth_key)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
%w[auth params strategy origin error error_type error_strategy].each do |data|
|
78
|
+
define_method(:"omniauth_#{data}") do
|
79
|
+
request.env.fetch("omniauth.#{data.tr("_", ".")}")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def omniauth_providers
|
84
|
+
self.class.instance_variable_get(:@omniauth_providers).map do |(provider, *args)|
|
85
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
86
|
+
options[:name] || provider
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def omniauth_run(app)
|
93
|
+
omniauth_around_run do
|
94
|
+
request.run app, not_found: :pass do |res|
|
95
|
+
handle_omniauth_response(res)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# returns rack app with all registered strategies added to the middleware stack
|
101
|
+
def build_omniauth_app
|
102
|
+
builder = OmniAuth::Builder.new
|
103
|
+
builder.options(
|
104
|
+
path_prefix: omniauth_prefix,
|
105
|
+
setup: -> (env) { env["rodauth.omniauth.instance"].send(:omniauth_setup) }
|
106
|
+
)
|
107
|
+
builder.configure do |config|
|
108
|
+
[:request_validation_phase, :before_request_phase, :before_callback_phase, :on_failure].each do |hook|
|
109
|
+
config.send(:"#{hook}=", -> (env) { env["rodauth.omniauth.instance"].send(:"omniauth_#{hook}") })
|
110
|
+
end
|
111
|
+
end
|
112
|
+
self.class.instance_variable_get(:@omniauth_providers).each do |(provider, *args)|
|
113
|
+
options = args.pop if args.last.is_a?(Hash)
|
114
|
+
builder.provider provider, *args, **(options || {})
|
115
|
+
end
|
116
|
+
builder.run -> (env) { [404, {}, []] } # pass through
|
117
|
+
builder
|
118
|
+
end
|
119
|
+
|
120
|
+
def omniauth_request_validation_phase
|
121
|
+
check_csrf if check_csrf?
|
122
|
+
end
|
123
|
+
|
124
|
+
def omniauth_before_request_phase
|
125
|
+
# can be overrridden to perform code before request phase
|
126
|
+
end
|
127
|
+
|
128
|
+
def omniauth_before_callback_phase
|
129
|
+
# can be overrridden to perform code before callback phase
|
130
|
+
end
|
131
|
+
|
132
|
+
def omniauth_setup
|
133
|
+
# can be overridden to setup the strategy
|
134
|
+
end
|
135
|
+
|
136
|
+
def omniauth_on_failure
|
137
|
+
if features.include?(:json) && use_json?
|
138
|
+
json_response[omniauth_error_type_key] = omniauth_error_type
|
139
|
+
end
|
140
|
+
|
141
|
+
set_redirect_error_status omniauth_failure_error_status
|
142
|
+
set_redirect_error_flash omniauth_failure_error_flash
|
143
|
+
redirect omniauth_failure_redirect
|
144
|
+
end
|
145
|
+
|
146
|
+
def omniauth_around_run
|
147
|
+
set_omniauth_rodauth do
|
148
|
+
set_omniauth_session do
|
149
|
+
yield
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Ensures the OmniAuth app uses the same session as Rodauth.
|
155
|
+
def set_omniauth_session(&block)
|
156
|
+
if features.include?(:jwt) && use_jwt?
|
157
|
+
set_omniauth_jwt_session(&block)
|
158
|
+
else
|
159
|
+
session # ensure "rack.session" is set when roda sessions plugin is used
|
160
|
+
yield
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Makes OmniAuth strategies use the JWT session hash.
|
165
|
+
def set_omniauth_jwt_session
|
166
|
+
rack_session = request.env["rack.session"]
|
167
|
+
request.env["rack.session"] = session
|
168
|
+
yield
|
169
|
+
ensure
|
170
|
+
request.env["rack.session"] = rack_session
|
171
|
+
end
|
172
|
+
|
173
|
+
# Makes the Rodauth instance accessible inside OmniAuth strategies
|
174
|
+
# and callbacks.
|
175
|
+
def set_omniauth_rodauth
|
176
|
+
request.env["rodauth.omniauth.instance"] = self
|
177
|
+
yield
|
178
|
+
ensure
|
179
|
+
request.env.delete("rodauth.omniauth.instance")
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns authorization URL when using the JSON feature.
|
183
|
+
def handle_omniauth_response(res)
|
184
|
+
return unless features.include?(:json) && use_json?
|
185
|
+
|
186
|
+
if res[0] == 302
|
187
|
+
json_response[omniauth_authorize_url_key] = res[1]["Location"]
|
188
|
+
return_json_response
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def omniauth_request?
|
193
|
+
request.env.key?("omniauth.strategy")
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.included(auth)
|
197
|
+
auth.extend ClassMethods
|
198
|
+
auth.instance_variable_set(:@omniauth_providers, [])
|
199
|
+
end
|
200
|
+
|
201
|
+
module ClassMethods
|
202
|
+
def inherited(subclass)
|
203
|
+
super
|
204
|
+
subclass.instance_variable_set(:@omniauth_providers, @omniauth_providers.clone)
|
205
|
+
end
|
206
|
+
|
207
|
+
def freeze
|
208
|
+
super
|
209
|
+
@omniauth_providers.freeze
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "rodauth-omniauth"
|
3
|
+
spec.version = "0.1.0"
|
4
|
+
spec.authors = ["Janko Marohnić"]
|
5
|
+
spec.email = ["janko@hey.com"]
|
6
|
+
|
7
|
+
spec.summary = "Rodauth extension for logging in and creating account via OmniAuth authentication."
|
8
|
+
spec.description = "Rodauth extension for logging in and creating account via OmniAuth authentication."
|
9
|
+
spec.homepage = "https://github.com/janko/rodauth-omniauth"
|
10
|
+
spec.license = "MIT"
|
11
|
+
|
12
|
+
spec.required_ruby_version = ">= 2.3"
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
16
|
+
|
17
|
+
spec.files = Dir["README.md", "LICENSE.txt", "*.gemspec", "lib/**/*"]
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "rodauth", "~> 2.0"
|
21
|
+
spec.add_dependency "omniauth", "~> 2.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "minitest-hooks"
|
25
|
+
spec.add_development_dependency "tilt"
|
26
|
+
spec.add_development_dependency "bcrypt"
|
27
|
+
spec.add_development_dependency "mail"
|
28
|
+
spec.add_development_dependency "net-smtp"
|
29
|
+
spec.add_development_dependency "capybara"
|
30
|
+
spec.add_development_dependency "jwt"
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rodauth-omniauth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Janko Marohnić
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rodauth
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: omniauth
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-hooks
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tilt
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bcrypt
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mail
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: net-smtp
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: capybara
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: jwt
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Rodauth extension for logging in and creating account via OmniAuth authentication.
|
154
|
+
email:
|
155
|
+
- janko@hey.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- LICENSE.txt
|
161
|
+
- README.md
|
162
|
+
- lib/rodauth/features/omniauth.rb
|
163
|
+
- lib/rodauth/features/omniauth_base.rb
|
164
|
+
- rodauth-omniauth.gemspec
|
165
|
+
homepage: https://github.com/janko/rodauth-omniauth
|
166
|
+
licenses:
|
167
|
+
- MIT
|
168
|
+
metadata:
|
169
|
+
homepage_uri: https://github.com/janko/rodauth-omniauth
|
170
|
+
source_code_uri: https://github.com/janko/rodauth-omniauth
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '2.3'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubygems_version: 3.3.3
|
187
|
+
signing_key:
|
188
|
+
specification_version: 4
|
189
|
+
summary: Rodauth extension for logging in and creating account via OmniAuth authentication.
|
190
|
+
test_files: []
|