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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37cca4affe70df1979f4400ae30f646a38667fb82ddeec80d8f52d754fcb822b
4
- data.tar.gz: 945cedb9bdf4cdd18ba1059039fa31b9cdff0d190f219dd88276dd6a5954697b
3
+ metadata.gz: e3eeadfdfeeba40e946b5b38e9c24233eea04c3ab11dd31c1d655fa7ca6a14bc
4
+ data.tar.gz: e1a66179b3ba519474045b4ddda420596fac477ed2f79b98f70d398fed83952d
5
5
  SHA512:
6
- metadata.gz: c0e1d90c7ab684fc2db22dead9f0edd8eadd7c91c3e61c5399f2f887fe8e783c0943e8c25717abac702c1110f102774e34af6c1a9444da56038d36217e0705fe
7
- data.tar.gz: 7077c5b9919eb2bd59709866fe00f79b6503fd4a265c66d31520948a24baddeedb19f51937a2d55ce3030fc3c8552f526a7ca33e5b570462fa034da34bd508bb
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
@@ -1,3 +1,4 @@
1
1
  ActiveSupport::Inflector.inflections(:en) do |inflect|
2
2
  inflect.acronym "NoPassword"
3
+ inflect.acronym "OAuth"
3
4
  end
@@ -1,3 +1,3 @@
1
1
  module NoPassword
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
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.2
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-01-31 00:00:00.000000000 Z
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.3.7
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