cookie_crypt 1.0.0
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +113 -0
- data/Rakefile +1 -0
- data/app/controllers/devise/cookie_crypt_controller.rb +142 -0
- data/app/views/devise/cookie_crypt/max_login_attempts_reached.html.erb +3 -0
- data/app/views/devise/cookie_crypt/show.html.erb +51 -0
- data/config/locales/en.yml +4 -0
- data/cookie_crypt.gemspec +32 -0
- data/lib/cookie_crypt/controllers/helpers.rb +39 -0
- data/lib/cookie_crypt/hooks/cookie_cryptable.rb +6 -0
- data/lib/cookie_crypt/models/cookie_cryptable.rb +20 -0
- data/lib/cookie_crypt/orm/active_record.rb +12 -0
- data/lib/cookie_crypt/rails.rb +7 -0
- data/lib/cookie_crypt/routes.rb +9 -0
- data/lib/cookie_crypt/schema.rb +8 -0
- data/lib/cookie_crypt/version.rb +3 -0
- data/lib/cookie_crypt.rb +24 -0
- data/lib/generators/active_record/cookie_crypt_generator.rb +14 -0
- data/lib/generators/active_record/templates/migration.rb +13 -0
- data/lib/generators/cookie_crypt/cookie_crypt_generator.rb +32 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 03a95f39e7dfef045da3c962fd52a2df5ace9e11
|
|
4
|
+
data.tar.gz: af04600f35989fb07666ec55558406210f16612f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 645fa5fe1fe957cd9d4157a0ecf1978ddf0cba504a720029a332239aa9b70b454058f7a64693978d4b12a419e6e167631fb83cee8bdceb5f6ec6cc87c25626cc
|
|
7
|
+
data.tar.gz: 938a738da2b1f352aeb1309fb6b1bb0036814dd48aa93c8999a98743aee687ac897d8b6288ef0a88a82b4d5d757cfb8573848e6e1da6fbee520de827227eb699
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (C) 2013 Louis Alridge
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
## Cookie Crypt
|
|
2
|
+
|
|
3
|
+
## Encrypted Cookie Two Factor Authentication for Devise
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
* User customizable security questions and answers
|
|
8
|
+
* Configurable max login attempts
|
|
9
|
+
* Per user level of control (Allow certain ips to bypass two-factor)
|
|
10
|
+
* Customizable views
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
### Initial Setup
|
|
15
|
+
|
|
16
|
+
In a Rails environment, require the gem in your Gemfile:
|
|
17
|
+
|
|
18
|
+
gem 'cookie_crypt'
|
|
19
|
+
|
|
20
|
+
Once that's done, run:
|
|
21
|
+
|
|
22
|
+
bundle install
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Automatic installation
|
|
26
|
+
|
|
27
|
+
In order to add encrypted cookie two factor authorization to a model, run the command:
|
|
28
|
+
|
|
29
|
+
bundle exec rails g cookie_crypt MODEL
|
|
30
|
+
|
|
31
|
+
Where MODEL is your model name (e.g. User or Admin). This generator will add `:cookie_cryptable` to your model
|
|
32
|
+
and create a migration in `db/migrate/`, which will add the required columns to your table.
|
|
33
|
+
|
|
34
|
+
NOTE: This will create a field called "username" on the table it is creating if that field does not already exist.
|
|
35
|
+
The fields are security_question_one, security_question_two, security_answer_one, security_answer_two,
|
|
36
|
+
agent_list, and cookie_crypt_attempts_count
|
|
37
|
+
|
|
38
|
+
Finally, run the migration with:
|
|
39
|
+
|
|
40
|
+
bundle exec rake db:migrate
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Manual installation
|
|
44
|
+
|
|
45
|
+
To manually enable encrypted cookie two factor authentication for the User model, you should add cookie_crypt to your devise line, like:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
devise :database_authenticatable, :registerable,
|
|
49
|
+
:recoverable, :rememberable, :trackable, :validatable, :cookie_cryptable
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Config setup
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
config.max_login_attempts = 3
|
|
56
|
+
config.cookie_deletion_time_frame = '30.days.from_now'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Customisation
|
|
60
|
+
|
|
61
|
+
By default encrypted cookie two factor authentication is enabled for each user, you can change it with this method in your User model:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
def need_two_factor_authentication?(request)
|
|
65
|
+
request.ip != '127.0.0.1'
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will disable two factor authentication for local users and just puts the code in the logs.
|
|
70
|
+
|
|
71
|
+
### Rationalle
|
|
72
|
+
|
|
73
|
+
Cookie Crypt uses a cookie stored on a user's machine to monitor their authentication status. The first time a user passes the initial login
|
|
74
|
+
they are passed to the cookie crypt controller which prevents further action. They are "logged in" in the devise sense, but have an additional hook
|
|
75
|
+
preventing them from performing any actions that arent in the cookie crypt controller until they bypass that auth. If this is first time they are going
|
|
76
|
+
through cookie_crypt, the user will be presented with several labeled text boxes asking them to input two security questions and two security answers.
|
|
77
|
+
After inputting this information they are authenticated and redirected to the root of the application but not given an auth cookie.
|
|
78
|
+
|
|
79
|
+
It is important to note that the security answers are not saved as plaintext in the database. They are encrypted and that output is matched against
|
|
80
|
+
whatever the user inputs for their answers in the future.
|
|
81
|
+
|
|
82
|
+
When the user attempts to login again, they will be displayed their two security questions and asked to answer them with their two security answers.
|
|
83
|
+
If successful, an auth cookie is generated on the user's machine based on the user's username and encrypted password. The cookie is username - application
|
|
84
|
+
specific. No two cookies should ever be the same if the username field is unique. After receiving their auth cookie, the user's user agent is logged and
|
|
85
|
+
they are sent to the root of the system as fully authenticated. If the user was unsuccessful in authenticating 3 (or more) times, they will be locked out
|
|
86
|
+
until their cookie_crypt_attempts_count is reset to 0.
|
|
87
|
+
|
|
88
|
+
### Two factor Defense
|
|
89
|
+
|
|
90
|
+
So a user now has an auth cookie and the server knows it gave an auth cookie to this user that possessed this user agent, what now? If that same user with
|
|
91
|
+
that same agent tries to login again, they will authenticate through cookie crypt auth without any work on their part. The server simply matches the value
|
|
92
|
+
of their cookie with what it expects it should be. If they match AND the user agent the user is using is in the list of agents allocated to that user,
|
|
93
|
+
everything is square and they are authenticated. Using the UserAgent gem, incremental updates in a user's user agent will not be treated as differing agents.
|
|
94
|
+
The system will log the attempt as successful and update the user's agent_list with the updated agent value.
|
|
95
|
+
|
|
96
|
+
But what if they're logging in through a different machine / browser? Then they input their security answers and are given a cookie for that agent.
|
|
97
|
+
|
|
98
|
+
But what if an attacker knows the user's username and password? The attacker must also know the user's security question answers to auth as the user.
|
|
99
|
+
|
|
100
|
+
But what if an attacker knows the user's username and password AND has a copy of the user's cookie in their browser? Cookie crypt detects this case and
|
|
101
|
+
locks out the attacker by referencing the agent_list. A user that has a cookie but not a validated agent is obviously an attacker. This case also creates a
|
|
102
|
+
file in Rails.root / log to notify the admins of a hacking attempt. The agent_list field stores information given out by a browser's user agent and contains a
|
|
103
|
+
decent enough amount of data for fingerprinting. More could be done in this regard (testing to see what fonts/plugins a browser has) but is outside the scope
|
|
104
|
+
of this gem and would make it more difficult for the gem to be only a minor inconvienence to the users.
|
|
105
|
+
|
|
106
|
+
What cookie crypt doesnt prevent:
|
|
107
|
+
An attacker that knows a user's username and password thats logging in from the user's machine / browser
|
|
108
|
+
An attacker that knows a user's username and password thats also spoofing the user's agent and also has the user's same auth-cookie
|
|
109
|
+
An attacker that knows a user's username, password, security questions and answers to said questions
|
|
110
|
+
|
|
111
|
+
Afterword: Spoofing a user agent is not that difficult, any modern browser with dev tools can change its user agent rather easily. The catch is that the values
|
|
112
|
+
need to match with what the user already has which requires additional work on the attacker's part. Also, The system recognizes updates to both the user's OS AND
|
|
113
|
+
browser.
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
include ActionView::Helpers::SanitizeHelper
|
|
2
|
+
require "useragent"
|
|
3
|
+
require "logger"
|
|
4
|
+
class Devise::CookieCryptController < DeviseController
|
|
5
|
+
prepend_before_filter :authenticate_scope!
|
|
6
|
+
before_filter :prepare_and_validate, :handle_cookie_crypt
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
if has_matching_encrypted_cookie?
|
|
10
|
+
if !using_an_agent_that_is_already_being_used?
|
|
11
|
+
#An attacker has successfully obtained a user's cookie and login credentials and is trying to pass themselves off as the target
|
|
12
|
+
#This is an attacker because the agent data does not match the agent data from when a cookie is generated for this user's machine.
|
|
13
|
+
#A machine that "suddenly" has a cookie despite not being auth'd is an attacker.
|
|
14
|
+
|
|
15
|
+
log_hack_attempt
|
|
16
|
+
|
|
17
|
+
resource.cookie_crypt_attempts_count = resource.class.max_cookie_crypt_login_attempts
|
|
18
|
+
resource.save #prevents attacker from deleting cookie and trying to login "normally" by inputting the user's two_fac answers
|
|
19
|
+
|
|
20
|
+
sign_out(resource)
|
|
21
|
+
redirect_to :root and return
|
|
22
|
+
else
|
|
23
|
+
authentication_success
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
flash[:notice] = "Signed In Successfully, now going through two factor authentication."
|
|
27
|
+
@user = resource
|
|
28
|
+
render template: "devise/cookie_crypt/show"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
if resource.security_question_one.blank? # initial case (first login)
|
|
34
|
+
|
|
35
|
+
resource.security_question_one = sanitize(params[:security_question_one])
|
|
36
|
+
resource.security_question_two = sanitize(params[:security_question_two])
|
|
37
|
+
resource.security_answer_one = Digest::SHA512.hexdigest(sanitize(params[:security_answer_one]))
|
|
38
|
+
resource.security_answer_two = Digest::SHA512.hexdigest(sanitize(params[:security_answer_two]))
|
|
39
|
+
resource.save
|
|
40
|
+
|
|
41
|
+
authentication_success
|
|
42
|
+
else
|
|
43
|
+
|
|
44
|
+
if matching_answers?
|
|
45
|
+
generate_cookie
|
|
46
|
+
log_agent_to_resource
|
|
47
|
+
authentication_success
|
|
48
|
+
else
|
|
49
|
+
resource.cookie_crypt_attempts_count += 1
|
|
50
|
+
resource.save
|
|
51
|
+
set_flash_message :error, :attempt_failed
|
|
52
|
+
if resource.max_cookie_crypt_login_attempts?
|
|
53
|
+
sign_out(resource)
|
|
54
|
+
render template: 'devise/cookie_crypt/max_login_attempts_reached' and return
|
|
55
|
+
else
|
|
56
|
+
render :show
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def authenticate_scope!
|
|
65
|
+
self.resource = send("current_#{resource_name}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def encrypted_username_and_pass
|
|
69
|
+
Digest::SHA512.hexdigest("#{resource.username}_#{resource.encrypted_password}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def generate_cookie
|
|
73
|
+
cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}".to_sym] = {
|
|
74
|
+
value: "#{encrypted_username_and_pass}",
|
|
75
|
+
expires: Date.class_eval("#{resource.class.cookie_deletion_time_frame}")
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def has_matching_encrypted_cookie?
|
|
80
|
+
cookies["#{resource.username}_#{Rails.application.class.to_s.split("::").first}"] == encrypted_username_and_pass
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def log_hack_attempt
|
|
84
|
+
logger = Logger.new("#{Rails.root.join('log','hack_attempts.log')}")
|
|
85
|
+
logger.warn "Attempt to bypass two factor authentication and devise detected from ip #{request.remote_ip} using #{resource_name}: #{resource.inspect}!"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def log_agent_to_resource
|
|
89
|
+
unless using_an_agent_that_is_already_being_used?
|
|
90
|
+
resource.agent_list = "#{resource.agent_list}#{'|' unless resource.agent_list.blank?}#{request.user_agent}"
|
|
91
|
+
resource.save
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def matching_answers?
|
|
96
|
+
resource.security_answer_one == Digest::SHA512.hexdigest(sanitize(params[:security_answer_one])) &&
|
|
97
|
+
resource.security_answer_two == Digest::SHA512.hexdigest(sanitize(params[:security_answer_two]))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def prepare_and_validate
|
|
101
|
+
redirect_to :root and return if resource.nil?
|
|
102
|
+
@limit = resource.class.max_cookie_crypt_login_attempts
|
|
103
|
+
if resource.max_cookie_crypt_login_attempts?
|
|
104
|
+
sign_out(resource)
|
|
105
|
+
render template: 'devise/cookie_crypt/max_login_attempts_reached' and return
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def authentication_success
|
|
110
|
+
flash[:notice] = 'Signed in through two-factor authentication successfully.'
|
|
111
|
+
warden.session(resource_name)[:need_cookie_crypt_auth] = false
|
|
112
|
+
sign_in resource_name, resource, :bypass => true
|
|
113
|
+
resource.update_attribute(:cookie_crypt_attempts_count, 0)
|
|
114
|
+
redirect_to stored_location_for(resource_name) || :root
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def using_an_agent_that_is_already_being_used?
|
|
118
|
+
unless resource.agent_list.blank?
|
|
119
|
+
request_agent = UserAgent.parse("#{request.user_agent}")
|
|
120
|
+
resource.agent_list.split('|').each do |agent_string|
|
|
121
|
+
if agent_string.include?("#{request_agent.application}")
|
|
122
|
+
agent = UserAgent.parse("#{agent_string}")
|
|
123
|
+
if agent.application == request_agent.application && agent.browser == request_agent.browser
|
|
124
|
+
if request_agent >= agent #version number is higher for example
|
|
125
|
+
#update user agent string and return true
|
|
126
|
+
resource.agent_list = resource.agent_list.gsub("#{agent.browser}/#{agent.version}","#{request_agent.browser}/#{request_agent.version}")
|
|
127
|
+
resource.save
|
|
128
|
+
return true
|
|
129
|
+
elsif request_agent.version == agent.version
|
|
130
|
+
return true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def unrecognized_agent?
|
|
140
|
+
resource.agent_list.include?("#{request.user_agent}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
<h2>Two Factor Login Page</h2>
|
|
3
|
+
|
|
4
|
+
<%=form_tag([resource_name, :cookie_crypt], method: :put) do %>
|
|
5
|
+
<% if @user.security_question_one.blank? %>
|
|
6
|
+
<h2>You have not yet setup two-factor questions and answers. Please follow the instructions below.</h2>
|
|
7
|
+
|
|
8
|
+
<h2>Note: It is a generally a good idea to have your questions be about people/events/objects that do not change over time.</h2>
|
|
9
|
+
|
|
10
|
+
<h3>Please input your first security question</h3>
|
|
11
|
+
|
|
12
|
+
<%=text_field_tag :security_question_one, nil, size: 50 %>
|
|
13
|
+
<br></br>
|
|
14
|
+
|
|
15
|
+
<h3>Please input your first question's answer</h3>
|
|
16
|
+
|
|
17
|
+
<%=text_field_tag :security_answer_one, nil, size: 50 %>
|
|
18
|
+
<br></br>
|
|
19
|
+
|
|
20
|
+
<h3>Please input your second security question </h3>
|
|
21
|
+
|
|
22
|
+
<%=text_field_tag :security_question_two, nil, size: 50 %>
|
|
23
|
+
<br></br>
|
|
24
|
+
|
|
25
|
+
<h3>Please input your second question's answer</h3>
|
|
26
|
+
|
|
27
|
+
<%=text_field_tag :security_answer_two, nil, size: 50 %>
|
|
28
|
+
<br></br>
|
|
29
|
+
|
|
30
|
+
<h2>Please note that you will not be given a security token to login by skipping two factor until you login next time.</h2>
|
|
31
|
+
|
|
32
|
+
<% else %>
|
|
33
|
+
|
|
34
|
+
<h2><%="#{@user.security_question_one}"%></h2>
|
|
35
|
+
|
|
36
|
+
<%=text_field_tag :security_answer_one, nil, size: 50 %>
|
|
37
|
+
|
|
38
|
+
<br></br>
|
|
39
|
+
|
|
40
|
+
<h2><%="#{@user.security_question_two}" %></h2>
|
|
41
|
+
|
|
42
|
+
<%=text_field_tag :security_answer_two, nil, size: 50 %>
|
|
43
|
+
|
|
44
|
+
<br></br>
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
47
|
+
<%= submit_tag "Submit" %>
|
|
48
|
+
|
|
49
|
+
<% end %>
|
|
50
|
+
<br></br>
|
|
51
|
+
<%=link_to "Sign out", destroy_user_session_path, :method => :delete %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "cookie_crypt/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "cookie_crypt"
|
|
7
|
+
s.version = CookieCrypt::VERSION.dup
|
|
8
|
+
s.authors = ["Dmitrii Golub","Louis Alridge"]
|
|
9
|
+
s.email = ["loualrid@gmail.com"]
|
|
10
|
+
s.homepage = "https://github.com/loualrid/CookieCrypt"
|
|
11
|
+
s.summary = %q{Encrypted cookie two factor authentication plugin for devise}
|
|
12
|
+
s.description = <<-EOF
|
|
13
|
+
### Features ###
|
|
14
|
+
* User customizable security questions and answers
|
|
15
|
+
* Configurable max login attempts
|
|
16
|
+
* per user level control if he really need two factor authentication
|
|
17
|
+
EOF
|
|
18
|
+
|
|
19
|
+
s.rubyforge_project = "cookie_crypt"
|
|
20
|
+
|
|
21
|
+
s.files = `git ls-files`.split("\n")
|
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
24
|
+
s.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
s.add_runtime_dependency 'rails', '>= 3.1.1'
|
|
27
|
+
s.add_runtime_dependency 'devise'
|
|
28
|
+
s.add_runtime_dependency 'useragent'
|
|
29
|
+
|
|
30
|
+
s.add_development_dependency 'bundler'
|
|
31
|
+
s.license = 'MIT'
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module CookieCrypt
|
|
2
|
+
module Controllers
|
|
3
|
+
module Helpers
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_filter :handle_cookie_crypt
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def handle_cookie_crypt
|
|
13
|
+
unless devise_controller?
|
|
14
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
|
15
|
+
if signed_in?(scope) and warden.session(scope)[:need_cookie_crypt_auth]
|
|
16
|
+
handle_failed_cookie_crypt_auth(scope)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def handle_failed_cookie_crypt_auth(scope)
|
|
23
|
+
if request.format.present? and request.format.html?
|
|
24
|
+
session["#{scope}_return_tor"] = request.path if request.get?
|
|
25
|
+
redirect_to cookie_crypt_auth_path_for(scope)
|
|
26
|
+
else
|
|
27
|
+
render nothing: true, status: :unauthorized
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cookie_crypt_auth_path_for(resource_or_scope = nil)
|
|
32
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
|
33
|
+
change_path = "#{scope}_cookie_crypt_path"
|
|
34
|
+
send(change_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'cookie_crypt/hooks/cookie_cryptable'
|
|
2
|
+
module Devise
|
|
3
|
+
module Models
|
|
4
|
+
module CookieCryptable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
::Devise::Models.config(self, :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def need_cookie_crypt_auth?(request)
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def max_cookie_crypt_login_attempts?
|
|
16
|
+
cookie_crypt_attempts_count >= self.class.max_cookie_crypt_login_attempts
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module CookieCrypt
|
|
2
|
+
module Orm
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Schema
|
|
5
|
+
include CookieCrypt::Schema
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
ActiveRecord::ConnectionAdapters::Table.send :include, CookieCrypt::Orm::ActiveRecord::Schema
|
|
12
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, CookieCrypt::Orm::ActiveRecord::Schema
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module ActionDispatch::Routing
|
|
2
|
+
class Mapper
|
|
3
|
+
protected
|
|
4
|
+
|
|
5
|
+
def devise_cookie_crypt(mapping, controllers)
|
|
6
|
+
resource :cookie_crypt, :only => [:show, :update], :path => mapping.path_names[:cookie_crypt], :controller => controllers[:cookie_crypt]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/cookie_crypt.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'cookie_crypt/version'
|
|
2
|
+
require 'devise'
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
|
|
6
|
+
module Devise
|
|
7
|
+
mattr_accessor :max_cookie_crypt_login_attempts, :cookie_deletion_time_frame
|
|
8
|
+
@@max_cookie_crypt_login_attempts = 3
|
|
9
|
+
@@cookie_deletion_time_frame = 30.days.from_now
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module CookieCrypt
|
|
13
|
+
autoload :Schema, 'cookie_crypt/schema'
|
|
14
|
+
module Controllers
|
|
15
|
+
autoload :Helpers, 'cookie_crypt/controllers/helpers'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Devise.add_module :cookie_cryptable, :model => 'cookie_crypt/models/cookie_cryptable', :controller => :cookie_crypt, :route => :cookie_crypt
|
|
20
|
+
|
|
21
|
+
require 'cookie_crypt/orm/active_record'
|
|
22
|
+
require 'cookie_crypt/routes'
|
|
23
|
+
require 'cookie_crypt/models/cookie_cryptable'
|
|
24
|
+
require 'cookie_crypt/rails'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'rails/generators/active_record'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Generators
|
|
5
|
+
class CookieCryptGenerator < ActiveRecord::Generators::Base
|
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
|
7
|
+
|
|
8
|
+
def copy_cookie_crypt_migration
|
|
9
|
+
migration_template "migration.rb", "db/migrate/cookie_crypt_add_to_#{table_name}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CookieCryptAddTo<%= table_name.camelize %> < ActiveRecord::Migration
|
|
2
|
+
def change
|
|
3
|
+
change_table :<%= table_name %> do |t|
|
|
4
|
+
<%='t.string :username, null: false, default: ""' if ActiveRecord::Base.class_eval("#{table_name.camelize.singularize}.inspect['username: string'].blank?") %>
|
|
5
|
+
t.string :security_question_one, default: ""
|
|
6
|
+
t.string :security_question_two, default: ""
|
|
7
|
+
t.string :security_answer_one, default: ""
|
|
8
|
+
t.string :security_answer_two, default: ""
|
|
9
|
+
t.text :agent_list, default: ""
|
|
10
|
+
t.integer :cookie_crypt_attempts_count, default: 0
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module CookieCryptable
|
|
2
|
+
module Generators
|
|
3
|
+
class CookieCryptGenerator < Rails::Generators::NamedBase
|
|
4
|
+
namespace "cookie_crypt"
|
|
5
|
+
desc "Adds :cookie_cryptable directive in the given model.
|
|
6
|
+
It also generates an active record migration."
|
|
7
|
+
|
|
8
|
+
def inject_cookie_crypt_content
|
|
9
|
+
paths = [File.join("app", "models", "#{file_path}.rb"),File.join("config", "initializers", "devise.rb")]
|
|
10
|
+
inject_into_file(paths[0], "cookie_cryptable, :", :after => "devise :") if File.exists?(paths[0])
|
|
11
|
+
if File.exists?(paths[1])
|
|
12
|
+
inject_into_file(paths[1], "\n # ==> Cookie Crypt Configuration Parameters\n config.max_cookie_crypt_login_attempts = 3
|
|
13
|
+
\n # For cookie_deletion_time_frame field, make sure your timeframe parses into an actual date and is a string
|
|
14
|
+
\n config.cookie_deletion_time_frame = '30.days.from_now'", after: "Devise.setup do |config|")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path('../../../../app/views/devise/cookie_crypt', __FILE__)
|
|
19
|
+
|
|
20
|
+
def generate_files
|
|
21
|
+
Dir.mkdir("app/views/devise") unless Dir.exists?("app/views/devise")
|
|
22
|
+
unless Dir.exists?("app/views/devise/cookie_crypt")
|
|
23
|
+
Dir.mkdir("app/views/devise/cookie_crypt")
|
|
24
|
+
copy_file "max_login_attempts_reached.html.erb", "app/views/devise/cookie_crypt/max_login_attempts_reached.html.erb"
|
|
25
|
+
copy_file "show.html.erb", "app/views/devise/cookie_crypt/show.html.erb"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
hook_for :orm
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cookie_crypt
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dmitrii Golub
|
|
8
|
+
- Louis Alridge
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2013-10-22 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: rails
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - '>='
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: 3.1.1
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - '>='
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: 3.1.1
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: devise
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - '>='
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: useragent
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - '>='
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - '>='
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: bundler
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - '>='
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - '>='
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
description: |2
|
|
71
|
+
### Features ###
|
|
72
|
+
* User customizable security questions and answers
|
|
73
|
+
* Configurable max login attempts
|
|
74
|
+
* per user level control if he really need two factor authentication
|
|
75
|
+
email:
|
|
76
|
+
- loualrid@gmail.com
|
|
77
|
+
executables: []
|
|
78
|
+
extensions: []
|
|
79
|
+
extra_rdoc_files: []
|
|
80
|
+
files:
|
|
81
|
+
- .gitignore
|
|
82
|
+
- Gemfile
|
|
83
|
+
- LICENSE
|
|
84
|
+
- README.md
|
|
85
|
+
- Rakefile
|
|
86
|
+
- app/controllers/devise/cookie_crypt_controller.rb
|
|
87
|
+
- app/views/devise/cookie_crypt/max_login_attempts_reached.html.erb
|
|
88
|
+
- app/views/devise/cookie_crypt/show.html.erb
|
|
89
|
+
- config/locales/en.yml
|
|
90
|
+
- cookie_crypt.gemspec
|
|
91
|
+
- lib/cookie_crypt.rb
|
|
92
|
+
- lib/cookie_crypt/controllers/helpers.rb
|
|
93
|
+
- lib/cookie_crypt/hooks/cookie_cryptable.rb
|
|
94
|
+
- lib/cookie_crypt/models/cookie_cryptable.rb
|
|
95
|
+
- lib/cookie_crypt/orm/active_record.rb
|
|
96
|
+
- lib/cookie_crypt/rails.rb
|
|
97
|
+
- lib/cookie_crypt/routes.rb
|
|
98
|
+
- lib/cookie_crypt/schema.rb
|
|
99
|
+
- lib/cookie_crypt/version.rb
|
|
100
|
+
- lib/generators/active_record/cookie_crypt_generator.rb
|
|
101
|
+
- lib/generators/active_record/templates/migration.rb
|
|
102
|
+
- lib/generators/cookie_crypt/cookie_crypt_generator.rb
|
|
103
|
+
homepage: https://github.com/loualrid/CookieCrypt
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata: {}
|
|
107
|
+
post_install_message:
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - '>='
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - '>='
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubyforge_project: cookie_crypt
|
|
123
|
+
rubygems_version: 2.0.6
|
|
124
|
+
signing_key:
|
|
125
|
+
specification_version: 4
|
|
126
|
+
summary: Encrypted cookie two factor authentication plugin for devise
|
|
127
|
+
test_files: []
|