nopassword 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +58 -0
- data/app/controllers/nopassword/oauth/google_authorizations_controller.rb +93 -0
- data/config/initializers/inflections.rb +1 -0
- data/lib/nopassword/version.rb +1 -1
- data/lib/nopassword.rb +9 -9
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3eeadfdfeeba40e946b5b38e9c24233eea04c3ab11dd31c1d655fa7ca6a14bc
|
4
|
+
data.tar.gz: e1a66179b3ba519474045b4ddda420596fac477ed2f79b98f70d398fed83952d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cf554d86cd5cd4d8bbaee896854b57b69345db9d6392f04e5b529a26b0388764aac352faa38ecd0c3bc746ac8fb138429c9d0b7f57077466efffd45410977d8
|
7
|
+
data.tar.gz: f5ff439783e2d6f47af1223df5c1fb279c3d0bb284ed950233f40cecdf7f93a21212987b84e039e1e326cb18c261f2d48d094587d57cc098f2ca886a1bc71233
|
data/README.md
CHANGED
@@ -135,6 +135,60 @@ Because of this modular approach, NoPassword can be used out of the box for many
|
|
135
135
|
|
136
136
|
NoPassword could be extended to work for other side-channel use cases too like login via SMS, QR code, etc.
|
137
137
|
|
138
|
+
## OAuth Authorizations
|
139
|
+
|
140
|
+
NoPassword has OAuth controllers that are designed to be the smallest possible integration with providers. To use them, create a controller in your Rails project and inherit from `NoPassword::OAuth::GoogleAuthorizationsController`
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
# ./app/controllers/google_authorizations_controller.rb
|
144
|
+
class GoogleAuthorizationsController < NoPassword::OAuth::GoogleAuthorizationsController
|
145
|
+
CLIENT_ID = ENV["GOOGLE_CLIENT_ID"]
|
146
|
+
CLIENT_SECRET = ENV["GOOGLE_CLIENT_SECRET"]
|
147
|
+
SCOPE = "openid email profile"
|
148
|
+
|
149
|
+
protected
|
150
|
+
# Here's what the callback returns.
|
151
|
+
# {"sub"=>"117509553887278399680",
|
152
|
+
# "name"=>"Brad Gessler",
|
153
|
+
# "given_name"=>"Brad",
|
154
|
+
# "family_name"=>"Gessler",
|
155
|
+
# "picture"=>"https://lh3.googleusercontent.com/a/AAcHTtcA4Mc7yx4ABlghdRp7GzkssdmccudQu6MhlItL259oTiJs=s96-c",
|
156
|
+
# "email"=>"brad@example.com",
|
157
|
+
# "email_verified"=>true,
|
158
|
+
# "locale"=>"en"}
|
159
|
+
def authorization_succeeded(user_info)
|
160
|
+
user = User.find_or_create_by(email: user_info.fetch("email"))
|
161
|
+
user ||= user_info.fetch("name")
|
162
|
+
|
163
|
+
self.current_user = user
|
164
|
+
redirect_to root_url
|
165
|
+
end
|
166
|
+
|
167
|
+
def authorization_failed
|
168
|
+
# Handle the error, perhaps redirect to a different login screen.
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
Then in `routes.rb` add the following:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
# ./config/routes.rb
|
177
|
+
resource :google_authorization
|
178
|
+
```
|
179
|
+
|
180
|
+
From your application, you'll need to kick off authorization flows by firing a non-Turbo POST request to `/google_authorization`. In this example, I create a `/google_authorization/new` page that is accessible via a `GET` request. In practice you'd probably make this a partial that you'd include on a `/sign-in` page.
|
181
|
+
|
182
|
+
```erb
|
183
|
+
<!-- ./app/views/google_authorization/new.html.erb -->
|
184
|
+
<h1>Login with Google</h1>
|
185
|
+
<% form_tag action: google_authorization_path do %>
|
186
|
+
<%= submit_tag "Login with Google" %>
|
187
|
+
<% end %>
|
188
|
+
```
|
189
|
+
|
190
|
+
Don't forget to login to the Google Developer Console at https://code.google.com/apis/console/ and get your API keys for the ENV vars above and add the `/google_authorization` URL to the domains Google is authorized to redirect back to.
|
191
|
+
|
138
192
|
## Motivations
|
139
193
|
|
140
194
|
Understanding why something was created is important to understanding it better.
|
@@ -149,10 +203,14 @@ The gems I evaluated all did more than I wanted them to:
|
|
149
203
|
|
150
204
|
NoPassword only worries about generating codes and creating a secure environment for end-users to validate the codes.
|
151
205
|
|
206
|
+
Additionally, as I was using OmniAuth in my projects to handle OAuth authorizations, I noticed it felt more difficult than it should be, so I started creating OAuth controllers to simplify the process. I've included them here in case you find them useful.
|
207
|
+
|
152
208
|
### Why was it not built on devise, warden, or ominauth?
|
153
209
|
|
154
210
|
I initially thought this would make for a great OmniAuth strategy, but quickly realized OmniAuth has a goal of being agnostic to rails and ships Rack middleware. I needed something more integrated into Rails controllers and views so that I could more easily extend in various projects.
|
155
211
|
|
212
|
+
Additionally, I found OmniAuth was more difficult to configure for OAuth in Rails than it should be. OmniAuth required a `./config/initializers/omniauth.rb` file that builds a Rack middleware. This Rack middleware then had some hard coded URLs that would be intercepted from Rails, which made it difficult to reason between controller routes and Rack middleware routes. The NoPassword::OAuth controllers solve that problem by having both configuration and logic live in a single controller file.
|
213
|
+
|
156
214
|
Devise already has [devise-passwordless](https://rubygems.org/gems/devise-passwordless), but it tries to do too much for my purposes by managing user authorization. I needed something that stopped short of managing user authorization.
|
157
215
|
|
158
216
|
## Contributing
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module NoPassword
|
2
|
+
class OAuth::GoogleAuthorizationsController < ApplicationController
|
3
|
+
CLIENT_ID = ENV["GOOGLE_CLIENT_ID"]
|
4
|
+
CLIENT_SECRET = ENV["GOOGLE_CLIENT_SECRET"]
|
5
|
+
SCOPE = "openid email profile"
|
6
|
+
|
7
|
+
AUTHORIZATION_URL = URI("https://accounts.google.com/o/oauth2/v2/auth")
|
8
|
+
TOKEN_URL = URI("https://www.googleapis.com/oauth2/v4/token")
|
9
|
+
USER_INFO_URL = URI("https://www.googleapis.com/oauth2/v3/userinfo")
|
10
|
+
|
11
|
+
before_action :validate_state_token, only: :show
|
12
|
+
|
13
|
+
def create
|
14
|
+
redirect_to authorization_url.to_s, allow_other_host: true
|
15
|
+
end
|
16
|
+
|
17
|
+
def show
|
18
|
+
access_token = request_access_token.parse.fetch("access_token")
|
19
|
+
user_info = request_user_info(access_token: access_token).parse
|
20
|
+
|
21
|
+
if user_info.any?
|
22
|
+
Rails.logger.info "Authorization #{self.class} succeeded"
|
23
|
+
authorization_succeeded user_info
|
24
|
+
else
|
25
|
+
Rails.logger.info "Authorization #{self.class} failed"
|
26
|
+
authorization_failed
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
def authorization_succeeded(user_info)
|
32
|
+
redirect_to root_url
|
33
|
+
end
|
34
|
+
|
35
|
+
def authorization_failed
|
36
|
+
raise "Implement authorization_failed to handle failed authorizations", NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_state_token
|
40
|
+
state_token = params.fetch(:state)
|
41
|
+
unless valid_authenticity_token?(session, state_token)
|
42
|
+
raise ActionController::InvalidAuthenticityToken, "The state=#{state_token} token is inauthentic."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generates the OAuth authorization URL that will redirect the user to the OAuth provider.
|
47
|
+
# A Rails `form_authenticity_token` is used as the `state` parameter to prevent CSRF. The
|
48
|
+
# `callback_url` is where the user is sent back to after authenticating with the OAuth provider.
|
49
|
+
def authorization_url
|
50
|
+
AUTHORIZATION_URL.build.query(
|
51
|
+
client_id: client_id,
|
52
|
+
redirect_uri: callback_url,
|
53
|
+
response_type: "code",
|
54
|
+
scope: scope,
|
55
|
+
state: form_authenticity_token
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Requests an OAuth access token from the OAuth provider. The access token is used for subsequent
|
60
|
+
# requests to gather information like a users name, email, address, or whatever other information
|
61
|
+
# The OAuth provider makes available.
|
62
|
+
def request_access_token
|
63
|
+
HTTP.post(TOKEN_URL, form: {
|
64
|
+
client_id: client_id,
|
65
|
+
client_secret: client_secret,
|
66
|
+
code: params.fetch(:code),
|
67
|
+
grant_type: "authorization_code",
|
68
|
+
redirect_uri: callback_url
|
69
|
+
})
|
70
|
+
end
|
71
|
+
|
72
|
+
def client_id
|
73
|
+
self.class::CLIENT_ID
|
74
|
+
end
|
75
|
+
|
76
|
+
def client_secret
|
77
|
+
self.class::CLIENT_SECRET
|
78
|
+
end
|
79
|
+
|
80
|
+
def scope
|
81
|
+
self.class::SCOPE
|
82
|
+
end
|
83
|
+
|
84
|
+
def request_user_info(access_token:)
|
85
|
+
HTTP.auth("Bearer #{access_token}").get(USER_INFO_URL)
|
86
|
+
end
|
87
|
+
|
88
|
+
# The URL the OAuth provider will redirect the user back to after authenticating.
|
89
|
+
def callback_url
|
90
|
+
url_for(action: :show, only_path: false)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/nopassword/version.rb
CHANGED
data/lib/nopassword.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
|
-
require "nopassword/version"
|
2
|
-
require "nopassword/encryptor"
|
3
|
-
require "nopassword/random_code_generator"
|
4
1
|
require "pathname"
|
2
|
+
require "zeitwerk"
|
5
3
|
|
6
4
|
module NoPassword
|
5
|
+
Loader = Zeitwerk::Loader.for_gem.tap do |loader|
|
6
|
+
loader.ignore "#{__dir__}/generators"
|
7
|
+
loader.inflector.inflect "nopassword" => "NoPassword"
|
8
|
+
loader.inflector.inflect "oauth" => "OAuth"
|
9
|
+
loader.setup
|
10
|
+
end
|
11
|
+
|
7
12
|
def self.root
|
8
13
|
Pathname.new(__dir__).join("..")
|
9
14
|
end
|
10
15
|
end
|
11
16
|
|
12
|
-
require "nopassword/engine"
|
13
|
-
|
14
|
-
# Blurg, without this require, the inflector won't properly inflect the `nopassword_engine:install:migrations`
|
15
|
-
# task from the `rails g install nopassword:install` task.
|
16
|
-
require_relative "../config/initializers/inflections.rb"
|
17
|
-
|
17
|
+
require "nopassword/engine" if defined? Rails
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nopassword
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brad Gessler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 7.0.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: zeitwerk
|
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'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rspec-rails
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -51,6 +65,7 @@ files:
|
|
51
65
|
- Rakefile
|
52
66
|
- app/assets/config/nopassword_manifest.js
|
53
67
|
- app/controllers/nopassword/email_authentications_controller.rb
|
68
|
+
- app/controllers/nopassword/oauth/google_authorizations_controller.rb
|
54
69
|
- app/mailers/application_mailer.rb
|
55
70
|
- app/mailers/nopassword/email_authentication_mailer.rb
|
56
71
|
- app/models/nopassword.rb
|
@@ -99,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
114
|
- !ruby/object:Gem::Version
|
100
115
|
version: '0'
|
101
116
|
requirements: []
|
102
|
-
rubygems_version: 3.
|
117
|
+
rubygems_version: 3.4.6
|
103
118
|
signing_key:
|
104
119
|
specification_version: 4
|
105
120
|
summary: Passwordless login to Rails applications via email
|