devise_revocable_session 0.1.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 +10 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/Rakefile +1 -0
- data/app/models/devise/revocable_session.rb +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/devise_revocable_session.gemspec +26 -0
- data/lib/devise_revocable_session/hooks/revocable_session.rb +41 -0
- data/lib/devise_revocable_session/models/revocable_session.rb +62 -0
- data/lib/devise_revocable_session/version.rb +3 -0
- data/lib/devise_revocable_session.rb +13 -0
- data/lib/generators/devise_revocable_session/devise_revocable_session_generator.rb +37 -0
- data/lib/generators/devise_revocable_session/templates/migration.rb +24 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7fd2174196e326e6415a40c53d1e34b9e365e4e530fb200b5444f6cac0f6c3ca
|
4
|
+
data.tar.gz: 944e658e4e52f08158215a2db81e9a9ffed681e9bdb791862f2d3c0317ae2d78
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 332723f1fc5f8eaec040ca9f876634ec3e54cac7cea558021f6626a37ea490ff63e8dc1aa83d09b910686775a19579688acc8074e93dd86bbd427f41d345513b
|
7
|
+
data.tar.gz: c4ccbd0380357329671590d6e8452affeca7873a8b3485ccf11b46a6499b47d1ca9e22cd58b63aec0f635fe204029669c16bac4f628b79a8c83e7aa3c5f1ee2b
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Carl Allen
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# DeviseRevocableSession
|
2
|
+
|
3
|
+
A module for devise to revoke sessions.
|
4
|
+
|
5
|
+
This is borrowed heavily from: https://github.com/mkhairi/devise_revocable and https://www.jonathanleighton.com/articles/2013/revocable-sessions-with-devise/
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'devise_revocable_session'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install devise_revocable_session
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Install using the generator
|
26
|
+
|
27
|
+
$ rails g devise_revocable_sessions
|
28
|
+
|
29
|
+
|
30
|
+
Add `:revocable_session` to your model's devise declaration
|
31
|
+
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/carlallen/devise_revocable_session. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "devise_revocable_session"
|
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(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "devise_revocable_session/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "devise_revocable_session"
|
6
|
+
spec.version = DeviseRevocableSession::VERSION
|
7
|
+
spec.authors = ['Carl Allen']
|
8
|
+
spec.email = ["github@allenofmn.com"]
|
9
|
+
|
10
|
+
spec.summary = 'A module for devise to revoke sessions'
|
11
|
+
spec.description = 'A module for devise to revoke sessions'
|
12
|
+
spec.homepage = 'http://github.com/carlallen/devise_revocable_session'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_dependency "devise"
|
25
|
+
spec.add_dependency "activemodel"
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# After authenticating, we’re removing any session activation that may already
|
2
|
+
# exist, and creating a new session# activation. We generate our own random id
|
3
|
+
# (in User#activate_session) and store it in the auth_id key. There is already
|
4
|
+
# a session_id key, but the session gets renewed (and the session id changes)
|
5
|
+
# after authentication in order to avoid session fixation attacks. So it’s
|
6
|
+
# easier to just use our own id.
|
7
|
+
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
|
8
|
+
scope = options[:scope]
|
9
|
+
if record && record.respond_to?(:has_revocable_sessions?) && record.has_revocable_sessions?
|
10
|
+
record.deactivate_session!(warden.raw_session[:auth_id])
|
11
|
+
revocable_session = record.activate_session(warden.request)
|
12
|
+
warden.raw_session[:auth_id] = revocable_session.session_id
|
13
|
+
warden.cookies[:device_id] = revocable_session.device_id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# After fetching a user from the session, we check that the session is marked
|
18
|
+
# as active for that record. If it’s not we log the user out.
|
19
|
+
Warden::Manager.after_fetch do |record, warden, options|
|
20
|
+
scope = options[:scope]
|
21
|
+
if record && record.respond_to?(:has_revocable_sessions?) && record.has_revocable_sessions?
|
22
|
+
device_cookie = warden.cookies[:device_id]
|
23
|
+
valid_session = record.session_active?(device_cookie, warden.raw_session[:auth_id])
|
24
|
+
if device_cookie.present? and valid_session
|
25
|
+
record.mark_last_seen!(device_cookie)
|
26
|
+
else
|
27
|
+
warden.logout(scope)
|
28
|
+
throw :warden, scope: scope, message: :unauthenticated
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# When logging out, we deactivate_session! the current session. This ensures that the
|
34
|
+
# session cookie can’t be reused afterwards.
|
35
|
+
Warden::Manager.before_logout do |record, warden, options|
|
36
|
+
scope = options[:scope]
|
37
|
+
if record && record.respond_to?(:has_revocable_sessions?) && record.has_revocable_sessions?
|
38
|
+
record.deactivate_session!(warden.raw_session[:auth_id])
|
39
|
+
warden.cookies.delete(:device_id)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Devise
|
2
|
+
module Models
|
3
|
+
module RevocableSession
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :revocable_sessions, dependent: :destroy,
|
8
|
+
as: :resource, class_name: "Devise::RevocableSession"
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_revocable_sessions?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
#session
|
16
|
+
def activate_session(request)
|
17
|
+
new_session = revocable_sessions.new(attrs_for_login(request))
|
18
|
+
new_session.session_id = SecureRandom.hex(64)
|
19
|
+
new_session.device_id = SecureRandom.uuid
|
20
|
+
new_session.signed_in_ip = request.remote_ip
|
21
|
+
new_session.save
|
22
|
+
purge_old_sessions
|
23
|
+
new_session
|
24
|
+
end
|
25
|
+
|
26
|
+
def exclusive_session(session_id)
|
27
|
+
revocable_sessions.where('session_id != ?', session_id).delete_all
|
28
|
+
end
|
29
|
+
|
30
|
+
def session_active?(device_id, session_id)
|
31
|
+
revocable_sessions.where(device_id: device_id, session_id: session_id).exists?
|
32
|
+
end
|
33
|
+
|
34
|
+
def deactivate_session!(session_id)
|
35
|
+
revocable_sessions.where(session_id: session_id).delete_all
|
36
|
+
end
|
37
|
+
|
38
|
+
def purge_old_sessions
|
39
|
+
revocable_sessions.order(created_at: :desc).offset(10).destroy_all
|
40
|
+
end
|
41
|
+
|
42
|
+
def mark_last_seen!(device_id)
|
43
|
+
login_record = revocable_sessions.find_by(device_id: device_id)
|
44
|
+
#skip second to reduce database hit
|
45
|
+
if (Time.now - (login_record.last_seen_at)) >= 60
|
46
|
+
login_record.update_column :last_seen_at, Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def attrs_for_login(request)
|
53
|
+
t = Time.now
|
54
|
+
{ user_agent: request.user_agent,
|
55
|
+
last_seen_ip: request.remote_ip,
|
56
|
+
signed_in_at: t,
|
57
|
+
last_seen_at: t }
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "devise_revocable_session/version"
|
2
|
+
require 'devise'
|
3
|
+
require 'devise_revocable_session/hooks/revocable_session'
|
4
|
+
require 'devise_revocable_session/models/revocable_session'
|
5
|
+
|
6
|
+
module DeviseRevocableSession
|
7
|
+
class Engine < ::Rails::Engine
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Devise.with_options model: true do |d|
|
12
|
+
d.add_module :revocable_session
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rails/generators/migration'
|
2
|
+
|
3
|
+
class DeviseRevocableSessionGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
if defined?(ActiveRecord)
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Generates a migration to store revocable sessions."
|
10
|
+
|
11
|
+
def self.source_root
|
12
|
+
@_devise_source_root ||= File.expand_path('../templates', __FILE__)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.next_migration_number(dirname)
|
16
|
+
if ActiveRecord::Base.timestamped_migrations
|
17
|
+
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
18
|
+
else
|
19
|
+
'%.3d' % (current_migration_number(dirname) + 1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate
|
24
|
+
create_migration_file
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def create_migration_file
|
31
|
+
migration_template 'migration.rb', "db/migrate/create_revocable_sessions.rb", migration_version: migration_version
|
32
|
+
end
|
33
|
+
|
34
|
+
def migration_version
|
35
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class CreateRevocableSessions < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :revocable_sessions do |t|
|
4
|
+
t.integer :resource_id, null: false
|
5
|
+
t.string :resource_type, null: false
|
6
|
+
t.string :session_id, null: false
|
7
|
+
t.string :device_id
|
8
|
+
t.string :user_agent
|
9
|
+
t.datetime :signed_in_at
|
10
|
+
t.string :signed_in_ip
|
11
|
+
t.datetime :last_seen_at
|
12
|
+
t.string :last_seen_ip
|
13
|
+
t.datetime :signed_out_at
|
14
|
+
t.boolean :active, default: true
|
15
|
+
|
16
|
+
t.index [:resource_id, :resource_type], name: :resource
|
17
|
+
t.index :session_id, unique: true
|
18
|
+
t.index :device_id
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: devise_revocable_session
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carl Allen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: devise
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
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: activemodel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A module for devise to revoke sessions
|
70
|
+
email:
|
71
|
+
- github@allenofmn.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- app/models/devise/revocable_session.rb
|
82
|
+
- bin/console
|
83
|
+
- bin/setup
|
84
|
+
- devise_revocable_session.gemspec
|
85
|
+
- lib/devise_revocable_session.rb
|
86
|
+
- lib/devise_revocable_session/hooks/revocable_session.rb
|
87
|
+
- lib/devise_revocable_session/models/revocable_session.rb
|
88
|
+
- lib/devise_revocable_session/version.rb
|
89
|
+
- lib/generators/devise_revocable_session/devise_revocable_session_generator.rb
|
90
|
+
- lib/generators/devise_revocable_session/templates/migration.rb
|
91
|
+
homepage: http://github.com/carlallen/devise_revocable_session
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubygems_version: 3.0.4
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: A module for devise to revoke sessions
|
114
|
+
test_files: []
|