omniauth-onetime 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +1158 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/Gemfile +4 -0
- data/README.md +244 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/omniauth/omniauth-onetime/version.rb +24 -0
- data/lib/omniauth/strategies/onetime.rb +205 -0
- data/lib/omniauth-onetime.rb +24 -0
- data/omniauth-onetime.gemspec +29 -0
- metadata +130 -0
data/COPYING.LESSER
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
2
|
+
Version 3, 29 June 2007
|
3
|
+
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
6
|
+
of this license document, but changing it is not allowed.
|
7
|
+
|
8
|
+
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
11
|
+
License, supplemented by the additional permissions listed below.
|
12
|
+
|
13
|
+
0. Additional Definitions.
|
14
|
+
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
17
|
+
General Public License.
|
18
|
+
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
20
|
+
other than an Application or a Combined Work as defined below.
|
21
|
+
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
25
|
+
of using an interface provided by the Library.
|
26
|
+
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
28
|
+
Application with the Library. The particular version of the Library
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
30
|
+
Version".
|
31
|
+
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
35
|
+
based on the Application, and not on the Linked Version.
|
36
|
+
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
38
|
+
object code and/or source code for the Application, including any data
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
41
|
+
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
43
|
+
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
46
|
+
|
47
|
+
2. Conveying Modified Versions.
|
48
|
+
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
51
|
+
that uses the facility (other than as an argument passed when the
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
53
|
+
version:
|
54
|
+
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
56
|
+
ensure that, in the event an Application does not supply the
|
57
|
+
function or data, the facility still operates, and performs
|
58
|
+
whatever part of its purpose remains meaningful, or
|
59
|
+
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
61
|
+
this License applicable to that copy.
|
62
|
+
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
64
|
+
|
65
|
+
The object code form of an Application may incorporate material from
|
66
|
+
a header file that is part of the Library. You may convey such object
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
68
|
+
material is not limited to numerical parameters, data structure
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
71
|
+
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
73
|
+
Library is used in it and that the Library and its use are
|
74
|
+
covered by this License.
|
75
|
+
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
77
|
+
document.
|
78
|
+
|
79
|
+
4. Combined Works.
|
80
|
+
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
82
|
+
taken together, effectively do not restrict modification of the
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
85
|
+
the following:
|
86
|
+
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
88
|
+
the Library is used in it and that the Library and its use are
|
89
|
+
covered by this License.
|
90
|
+
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
92
|
+
document.
|
93
|
+
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
95
|
+
execution, include the copyright notice for the Library among
|
96
|
+
these notices, as well as a reference directing the user to the
|
97
|
+
copies of the GNU GPL and this license document.
|
98
|
+
|
99
|
+
d) Do one of the following:
|
100
|
+
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
102
|
+
License, and the Corresponding Application Code in a form
|
103
|
+
suitable for, and under terms that permit, the user to
|
104
|
+
recombine or relink the Application with a modified version of
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
107
|
+
Corresponding Source.
|
108
|
+
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
111
|
+
a copy of the Library already present on the user's computer
|
112
|
+
system, and (b) will operate properly with a modified version
|
113
|
+
of the Library that is interface-compatible with the Linked
|
114
|
+
Version.
|
115
|
+
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
117
|
+
be required to provide such information under section 6 of the
|
118
|
+
GNU GPL, and only to the extent that such information is
|
119
|
+
necessary to install and execute a modified version of the
|
120
|
+
Combined Work produced by recombining or relinking the
|
121
|
+
Application with a modified version of the Linked Version. (If
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
126
|
+
for conveying Corresponding Source.)
|
127
|
+
|
128
|
+
5. Combined Libraries.
|
129
|
+
|
130
|
+
You may place library facilities that are a work based on the
|
131
|
+
Library side by side in a single library together with other library
|
132
|
+
facilities that are not Applications and are not covered by this
|
133
|
+
License, and convey such a combined library under terms of your
|
134
|
+
choice, if you do both of the following:
|
135
|
+
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
137
|
+
on the Library, uncombined with any other library facilities,
|
138
|
+
conveyed under the terms of this License.
|
139
|
+
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
141
|
+
is a work based on the Library, and explaining where to find the
|
142
|
+
accompanying uncombined form of the same work.
|
143
|
+
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
145
|
+
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
148
|
+
versions will be similar in spirit to the present version, but may
|
149
|
+
differ in detail to address new problems or concerns.
|
150
|
+
|
151
|
+
Each version is given a distinguishing version number. If the
|
152
|
+
Library as you received it specifies that a certain numbered version
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
154
|
+
applies to it, you have the option of following the terms and
|
155
|
+
conditions either of that published version or of any later version
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
160
|
+
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
164
|
+
permanent authorization for you to choose that version for the
|
165
|
+
Library.
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
# OmniAuth One-Time
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/thoughtafter/omniauth-onetime)
|
4
|
+
[](https://codeclimate.com/github/thoughtafter/omniauth-onetime/coverage)
|
5
|
+
[](https://codeclimate.com/github/thoughtafter/omniauth-onetime)
|
6
|
+
|
7
|
+
An [OmniAuth](https://github.com/omniauth/omniauth) strategy using secure
|
8
|
+
onetime passwords.
|
9
|
+
|
10
|
+
I released this code on Dec 14, 2016 after having used it in production for some
|
11
|
+
time. Coincidentally, this is the same day that
|
12
|
+
[Yahoo disclosed a breach of 1 billion accounts](https://yahoo.tumblr.com/post/154479236569/important-security-information-for-yahoo-users)
|
13
|
+
which may have included MD5 hashed passwords. In the wake of this and numerous
|
14
|
+
other password breaches every web development team needs to ask:
|
15
|
+
|
16
|
+
**Is it worth storing long term user generated passwords?**
|
17
|
+
|
18
|
+
I suggest that it very rarely is.
|
19
|
+
|
20
|
+
## Vision
|
21
|
+
|
22
|
+
Strong passwords are difficult to remember. Rememberable passwords are usually
|
23
|
+
weak. Thus asking users to create and remember strong passwords is a limited
|
24
|
+
proposition.
|
25
|
+
|
26
|
+
The purpose of this gem is to provide a way for developers to quickly add a
|
27
|
+
secure authentication system to Rack based projects. This system is based on
|
28
|
+
one-time passwords which are emailed to the user at sign in time. These
|
29
|
+
passwords quickly expire (5 minute default, expiration time is configurable)
|
30
|
+
in order to thwart brute-force attacks.
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
Add this line to your application's Gemfile:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'omniauth-onetime'
|
38
|
+
```
|
39
|
+
|
40
|
+
And then execute:
|
41
|
+
|
42
|
+
$ bundle
|
43
|
+
|
44
|
+
Or install it yourself as:
|
45
|
+
|
46
|
+
$ gem install omniauth-onetime
|
47
|
+
|
48
|
+
## Usage
|
49
|
+
|
50
|
+
Enable omniauth-onetime using a Rails initializer at
|
51
|
+
`config/initializers/omniauth.rb`:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
55
|
+
provider :onetime
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
`config/routes.rb` file something like this:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
get '/auth/:provider/callback', to: 'sessions#create'
|
63
|
+
```
|
64
|
+
|
65
|
+
`app/controllers/sessions_controller.rb` file something like this:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
class SessionsController < ApplicationController
|
69
|
+
def create
|
70
|
+
@user = User.omniauth(auth_hash)
|
71
|
+
session[:user_id] = user.id
|
72
|
+
redirect_to request.env['omniauth.origin'] || root_path,
|
73
|
+
notice: "Signed in!"
|
74
|
+
end
|
75
|
+
|
76
|
+
def destroy
|
77
|
+
session[:user_id] = nil
|
78
|
+
redirect_to root_url, notice: "Signed out!"
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def auth_hash
|
84
|
+
request.env['omniauth.auth']
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
`app/models/user.rb` file something like this:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class User < ActiveRecord::Base
|
93
|
+
def self.omniauth(auth)
|
94
|
+
User.find_or_create_by!(provider: auth['provider'], email: auth['email'])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
### Configuration
|
100
|
+
|
101
|
+
These settings can be passed as a hash in the initializer.
|
102
|
+
|
103
|
+
* password_length - length of generated random passwords (default: 8)
|
104
|
+
* password_time - time in seconds that a generated password is valid
|
105
|
+
(default: 300)
|
106
|
+
* password_cost - bcrypt cost/rounds (default: 12), be sure you understand the
|
107
|
+
implications of changing this
|
108
|
+
* email_options - a hash to be sent to ActionMailer::Base.mail (default:
|
109
|
+
{ subject: 'Sign In Details' })
|
110
|
+
* password_cache - a cache to store the passwords (default: Rails.cache for
|
111
|
+
Rails apps, none otherwise), expected to function like
|
112
|
+
[ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html),
|
113
|
+
make sure this cache is appropriate for your deployment
|
114
|
+
* must implement: write, read, delete, exist?
|
115
|
+
|
116
|
+
## Details
|
117
|
+
|
118
|
+
Reading List:
|
119
|
+
|
120
|
+
* 2014-04-12: [Passwords are Obsolete](https://medium.com/@ninjudd/passwords-are-obsolete-9ed56d483eb)
|
121
|
+
* 2014-10-15: [Passwordless authentication: Secure, simple, and fast to deploy](https://hacks.mozilla.org/2014/10/passwordless-authentication-secure-simple-and-fast-to-deploy/)
|
122
|
+
* 2015-06-30: [Why passwords suck](https://medium.engineering/why-passwords-suck-d1d1f38c1bb4)
|
123
|
+
* 2016-08-12: [Securing access to genetic and personal information without a password](https://biogeniq.ca/en/articles/securing-access-to-genetic-and-personal-information-without-a-password/)
|
124
|
+
* [Passwordless](https://passwordless.net/)
|
125
|
+
|
126
|
+
The nomenclature has been settling on calling this approach "passwordless".
|
127
|
+
That makes sense if the email sent the user contains a link such that the user
|
128
|
+
never has to enter a password. However, this gem assumes that the device
|
129
|
+
receiving the password and the device being used to sign in may not be the same
|
130
|
+
and thus a password that can be read and entered is also a requirement. This
|
131
|
+
password can be used as a token for a "passwordless" authentication link. This
|
132
|
+
gem is "passwordless" but I believe more accurately it uses out-of-band
|
133
|
+
transmission of quickly expiring one-time passwords.
|
134
|
+
|
135
|
+
This approach may sound counter-intuitive, especially with a default password
|
136
|
+
length of 8 characters. However, the real key to the security is that the
|
137
|
+
passwords are sufficiently random and the window of opportunity is very short.
|
138
|
+
A traditional username / password system fails because the passwords are not
|
139
|
+
sufficiently random as truly random passwords are difficult to remember and
|
140
|
+
because the password lifetime is often very long. An issue which compounds
|
141
|
+
these traditional systems is password reuse which puts accounts at risk
|
142
|
+
whenever any system containing a user's password becomes compromised and
|
143
|
+
subject to a brute force attack.
|
144
|
+
|
145
|
+
### Benefits:
|
146
|
+
|
147
|
+
* No passwords to create - Users are generally very bad at creating passwords
|
148
|
+
unless they are somewhat knowledgeable and highly disciplined. The requirement
|
149
|
+
to remember the password is directly at odds with the strength of the password.
|
150
|
+
See also: [xkcd: Password Strength](https://xkcd.com/936/)
|
151
|
+
* No passwords to remember - Passwords can be random and arbitrarily
|
152
|
+
strong by increasing password length.
|
153
|
+
* Passwords are short lived - Brute-force attacks are thwarted even with
|
154
|
+
shorter passwords such as 8 random letters.
|
155
|
+
* Passwords are not reused - A system using this gem cannot divulge useful
|
156
|
+
password secrets if compromised. It is also immune from secrets divulged from
|
157
|
+
other systems, whether they use this gem or not. It also does not require user
|
158
|
+
trust that the website will not use passwords submitted by users for nefarious
|
159
|
+
ends. See also: [xkcd: Password Reuse](https://xkcd.com/792/)
|
160
|
+
* Easy for users - To Sign In:
|
161
|
+
1. enter your email
|
162
|
+
2. enter the password that has been emailed to you or click a link in the
|
163
|
+
email.
|
164
|
+
|
165
|
+
### Limitations:
|
166
|
+
|
167
|
+
* A compromised email account will compromise the user account. **However, this
|
168
|
+
is also true of any traditional password system that allows for email reset or
|
169
|
+
recovery of passwords.** The only way to circumvent this attack vector is to
|
170
|
+
handle password resets in a way that verifies a person's identity manually and
|
171
|
+
likely in person and with identification. Since most websites are probably not
|
172
|
+
willing to take that step (though financial institutions should be at the very
|
173
|
+
least considering it) then emailed one-time passwords are just as secure as
|
174
|
+
any website employing an automated email password reset system.
|
175
|
+
* Users must divulge an email account under their control to sign in. This does
|
176
|
+
not seem like a huge hurdle. If people are concerned with their privacy they
|
177
|
+
would likely have to create an anonymous/pseudonymous email for use with these
|
178
|
+
systems. Many websites require divulging an email even with a traditional
|
179
|
+
password system.
|
180
|
+
* Password emails can potentially create a log of usage. The existence of the
|
181
|
+
email cannot prove a user signed in or was trying to sign in since such emails
|
182
|
+
can be triggered by anyone. However, this is still a notable difference from
|
183
|
+
traditional systems.
|
184
|
+
* Relies on external email systems to deliver passwords. Any downtime in either
|
185
|
+
the email service used by the system or that used by the user will disrupt the
|
186
|
+
user's ability to sign in.
|
187
|
+
|
188
|
+
### Brute-force attacks:
|
189
|
+
|
190
|
+
Let's assume a malicious agent wants to brute-force a user's password on a
|
191
|
+
system using this gem. Using the default settings of an 8 letter password:
|
192
|
+
|
193
|
+
26^8 = 208,827,064,576 permutations
|
194
|
+
|
195
|
+
In order to compromise a password in 5 minutes an adversary will have to
|
196
|
+
hash nearly 700 million passwords per second.
|
197
|
+
|
198
|
+
26^8 permutations / 300 seconds = 696,090,216 hashes per second
|
199
|
+
|
200
|
+
This is far beyond what computing power can deliver for the bcrypt with a
|
201
|
+
default cost of 12. This scenario also assumes instantaneous access to the
|
202
|
+
stored crypted passwords which is highly unlikely without a greater breach of
|
203
|
+
security having already occurred.
|
204
|
+
|
205
|
+
On my development system the speed of bcrypt at cost 12 is roughly
|
206
|
+
4 hashes per second per core. A 2015 attempt to crack bcrypt passwords with a
|
207
|
+
cost of 12 using a GPU was able to achieve
|
208
|
+
[156 hashes per second per GPU](http://www.pxdojo.net/2015/08/what-i-learned-from-cracking-4000.html "What I learned from cracking 4000 Ashley Madison passwords").
|
209
|
+
A Zynq 7045 FPGA device was able to achieve
|
210
|
+
[226 hashes per second](http://www.openwall.com/presentations/Passwords14-Energy-Efficient-Cracking/slide-50.html "Energy-efficient bcrypt cracking, slide 50")
|
211
|
+
at bcrypt cost 12.
|
212
|
+
|
213
|
+
It's probably wise to keep this in mind:
|
214
|
+
|
215
|
+
[](https://xkcd.com/538/)
|
216
|
+
|
217
|
+
## Development
|
218
|
+
|
219
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
220
|
+
|
221
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
222
|
+
|
223
|
+
## Contributing
|
224
|
+
|
225
|
+
Bug reports and pull requests are welcome on GitHub at
|
226
|
+
https://github.com/thoughtafter/omniauth-onetime. This project is intended to be
|
227
|
+
a safe, welcoming space for collaboration, and contributors are expected to
|
228
|
+
adhere to the [Contributor Covenant](http://contributor-covenant.org) code of
|
229
|
+
conduct.
|
230
|
+
|
231
|
+
## License
|
232
|
+
|
233
|
+
omniauth-onetime is free software: you can redistribute it and/or modify
|
234
|
+
it under the terms of the GNU Lesser General Public License as published by
|
235
|
+
the Free Software Foundation, either version 3 of the License, or
|
236
|
+
(at your option) any later version.
|
237
|
+
|
238
|
+
omniauth-onetime is distributed in the hope that it will be useful,
|
239
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
240
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
241
|
+
GNU Lesser General Public License for more details.
|
242
|
+
|
243
|
+
You should have received a copy of the GNU Lesser General Public License
|
244
|
+
along with omniauth-onetime. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'omniauth-onetime'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#
|
2
|
+
# omniauth-onetime - An omniauth strategy using secure onetime passwords.
|
3
|
+
# Copyright (C) 2016 thoughtafter@gmail.com
|
4
|
+
#
|
5
|
+
# This file is part of omniauth-onetime.
|
6
|
+
#
|
7
|
+
# omniauth-onetime is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# omniauth-onetime is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU Lesser General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with omniauth-onetime. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
#
|
20
|
+
module OmniAuth
|
21
|
+
module Onetime
|
22
|
+
VERSION = '1.0.3'.freeze
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
#
|
2
|
+
# omniauth-onetime - An omniauth strategy using secure onetime passwords.
|
3
|
+
# Copyright (C) 2016 thoughtafter@gmail.com
|
4
|
+
#
|
5
|
+
# This file is part of omniauth-onetime.
|
6
|
+
#
|
7
|
+
# omniauth-onetime is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# omniauth-onetime is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU Lesser General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with omniauth-onetime. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
#
|
20
|
+
module OmniAuth
|
21
|
+
module Strategies
|
22
|
+
# An omniauth strategy using secure onetime passwords
|
23
|
+
class Onetime
|
24
|
+
include OmniAuth::Strategy
|
25
|
+
|
26
|
+
option :password_length, 8
|
27
|
+
option :password_time, 300
|
28
|
+
option :password_cost, 12
|
29
|
+
option :password_cache, nil
|
30
|
+
option :email_options, subject: 'Sign In Details'
|
31
|
+
|
32
|
+
# these options are a means of modeling a theoretical adversary and
|
33
|
+
# ensuring some minimum level of security against that adversary
|
34
|
+
# the default is roughly a cluster of 100 GPU's, this is not inexpensive
|
35
|
+
# keep this in mind: https://xkcd.com/538/
|
36
|
+
# cost = bcrypt cost
|
37
|
+
# speed = hashes per second per device at cost
|
38
|
+
# devices = number of devices
|
39
|
+
AdversarySingleDevice = { cost: 12, speed: 300, devices: 1 }
|
40
|
+
AdversaryMultiDevice = { cost: 12, speed: 300, devices: 128 }
|
41
|
+
option :adversary, AdversaryMultiDevice
|
42
|
+
|
43
|
+
# 1 = 100 percent chance of the adversary cracking within the time
|
44
|
+
# 100 = 1% chance, 1000 = 0.1% chance, 10_000 = 0.01%
|
45
|
+
# or, 10_000 means there is 1 in 10,000 chance of brute-forcing a password
|
46
|
+
# in the time allotted
|
47
|
+
option :minimum_security, 10_000
|
48
|
+
|
49
|
+
def initialize(app, *args, &block)
|
50
|
+
super
|
51
|
+
|
52
|
+
if options[:password_cache].nil? && defined?(Rails)
|
53
|
+
options[:password_cache] = Rails.cache
|
54
|
+
end
|
55
|
+
|
56
|
+
if options[:password_cache].nil?
|
57
|
+
raise 'omniauth-onetime must be configured with a password cache.'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# hashes per second needed for 100% complete brute force
|
62
|
+
# higher is more secure
|
63
|
+
def self.difficulty
|
64
|
+
(26**default_options[:password_length]) /
|
65
|
+
default_options[:password_time]
|
66
|
+
end
|
67
|
+
|
68
|
+
# factor to adjust bcrypt costs
|
69
|
+
def self.adversary_adjust
|
70
|
+
2**(default_options[:adversary][:cost] -
|
71
|
+
default_options[:password_cost])
|
72
|
+
end
|
73
|
+
|
74
|
+
# hashes per second (total) at password_cost
|
75
|
+
def self.adversary_speed
|
76
|
+
default_options[:adversary][:speed] *
|
77
|
+
default_options[:adversary][:devices] * adversary_adjust
|
78
|
+
end
|
79
|
+
|
80
|
+
# ratio of hashes per second needed to brute-force to the theoretical
|
81
|
+
# adversary, <= 1 means the adversary can crack within the time alloted
|
82
|
+
# higher is more secure, chance of cracking = 1 in adversary_ratio
|
83
|
+
def self.adversary_ratio
|
84
|
+
Rational(difficulty, adversary_speed)
|
85
|
+
end
|
86
|
+
|
87
|
+
# percentage chance of the adversary cracking the password
|
88
|
+
def self.adversary_chance
|
89
|
+
100 / adversary_ratio
|
90
|
+
end
|
91
|
+
|
92
|
+
class_eval do
|
93
|
+
if (s = adversary_ratio) < (m = default_options[:minimum_security])
|
94
|
+
raise ArgumentError, 'Omniauth-Onetime options do not reach minimum' \
|
95
|
+
" security requirements (#{s.to_i}<#{m}), please increase" \
|
96
|
+
' password_length, increase password_cost, or decrease password_time.'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# generate password of options[:password_length] length of uppercase
|
103
|
+
# letters A-Z
|
104
|
+
def new_password
|
105
|
+
Array.new(options[:password_length]) do
|
106
|
+
SecureRandom.random_number(26) + 65
|
107
|
+
end.pack('c*')
|
108
|
+
end
|
109
|
+
|
110
|
+
# create cryoted oassword from plaintext using Bcrypt and save it to the
|
111
|
+
# password cache
|
112
|
+
def save_password(email, plaintext)
|
113
|
+
crypted = BCrypt::Password
|
114
|
+
.create(plaintext, cost: options[:password_cost])
|
115
|
+
options[:password_cache]
|
116
|
+
.write(email, crypted, expires_in: options[:password_time])
|
117
|
+
end
|
118
|
+
|
119
|
+
# verify password, case is insensitive and all spaces and characters
|
120
|
+
# other than A-Z are stripped out
|
121
|
+
def verify_password(email, plaintext)
|
122
|
+
crypted = options[:password_cache].read(email)
|
123
|
+
|
124
|
+
begin
|
125
|
+
(BCrypt::Password.new(crypted) == plaintext.upcase.gsub(/\W/, ''))
|
126
|
+
rescue BCrypt::Errors::InvalidHash
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
uid do
|
132
|
+
request.env['omniauth.params']['email']
|
133
|
+
end
|
134
|
+
|
135
|
+
info do
|
136
|
+
{
|
137
|
+
name: uid,
|
138
|
+
email: uid
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def send_password(email, plaintext)
|
143
|
+
# break the password into groups of 4 letters for readability and
|
144
|
+
# usability
|
145
|
+
body = plaintext.scan(/.{4}/).join(' ')
|
146
|
+
ActionMailer::Base
|
147
|
+
.mail(options[:email_options].merge(to: email, body: body))
|
148
|
+
.deliver_now
|
149
|
+
end
|
150
|
+
|
151
|
+
def prepare_password(email)
|
152
|
+
# to prevent DOS do not send another password until previous one has
|
153
|
+
# expired
|
154
|
+
unless options[:password_cache].exist?(email)
|
155
|
+
plaintext = new_password
|
156
|
+
save_password(email, plaintext)
|
157
|
+
send_password(email, plaintext)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def request_email
|
162
|
+
log :debug, 'STEP 1: Ask user for email'
|
163
|
+
|
164
|
+
form = OmniAuth::Form.new(title: 'User Info')
|
165
|
+
form.text_field :email, :email
|
166
|
+
form.button 'Request Password'
|
167
|
+
form.to_response
|
168
|
+
end
|
169
|
+
|
170
|
+
def request_password(email)
|
171
|
+
log :debug, 'STEP 2: prepare password then ask user for password'
|
172
|
+
prepare_password(email)
|
173
|
+
|
174
|
+
form = OmniAuth::Form.new(title: 'User Info')
|
175
|
+
form.text_field :password, :password
|
176
|
+
form.html("<input type=\"hidden\" name=\"email\" value=\"#{email}\">")
|
177
|
+
form.button 'Sign In'
|
178
|
+
form.to_response
|
179
|
+
end
|
180
|
+
|
181
|
+
def request_verification(email, plaintext)
|
182
|
+
log :debug, 'STEP 3: verify password'
|
183
|
+
if verify_password(email, plaintext)
|
184
|
+
options[:password_cache].delete(email) # expire password
|
185
|
+
redirect callback_path
|
186
|
+
else
|
187
|
+
redirect request_path
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def request_phase
|
192
|
+
email = request.params['email']
|
193
|
+
plaintext = request.params['password']
|
194
|
+
|
195
|
+
if email.blank?
|
196
|
+
request_email
|
197
|
+
elsif plaintext.blank?
|
198
|
+
request_password(email)
|
199
|
+
else
|
200
|
+
request_verification(email, plaintext)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|