devise-login-cookie 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+
5
+ Gemfile.lock
data/README.md CHANGED
@@ -1,39 +1,110 @@
1
1
  devise-login-cookie
2
2
  ===================
3
3
 
4
- An extension for Devise which sets a signed login cookie upon authentication, making shared logins between same-domain applications possible.
4
+ A simple [Devise][1] extension for Single Sign On across same-domain web applications, using an [HMAC][2] signed login cookie. The cookie expiry is session-bound, and also contains a tamper-proof creation timestamp for server-enforced expiry.
5
+
6
+ [`OpenSSL::HMAC`][3] signing is performed by [`signed_json`][4] which also has implementations in PHP and Python, and can easily be implemented in other languages with OpenSSL and JSON. Note that standard Rails signed cookies are not appropriate for cross-platform use, as they use `Marshal.dump` internally.
7
+
8
+ [1]: https://github.com/plataformatec/devise
9
+ [2]: http://en.wikipedia.org/wiki/HMAC
10
+ [3]: http://ruby-doc.org/ruby-1.9/classes/OpenSSL/HMAC.html
11
+ [4]: https://github.com/pda/signed_json
5
12
 
6
13
 
7
14
  Installation
8
15
  ------------
9
16
 
17
+ Simple:
18
+
10
19
  gem install devise-login-cookie
11
20
 
21
+ Bundler-style:
22
+
12
23
  echo 'gem "devise-login-cookie"' >> Gemfile
13
24
  bundle install
14
25
 
15
- # load in config/initializers/devise
16
- require 'devise-login-cookie'
17
-
18
-
19
- Information
20
- -----------
21
-
22
- While Devise sets a cookie for Remember Me logins, standard logins are only tracked in the session.
23
- This extension sets a separate cookie upon authentication.
26
+ Rails:
24
27
 
25
- For the `:user` scope, the cookie is named `login_user_token`, consistent with `remember_user_token` from rememberable.
26
-
27
- The cookie is deleted via the before_logout Warden hook.
28
-
29
-
30
- TODO
31
- ----
32
-
33
- * Cookie is write-only; create a Warden strategy to consume cookie for login.
34
-
35
-
36
- Meh
37
- ---
38
-
39
- (c) 2010 Paul Annesley, MIT license.
28
+ # in config/initializers/devise.rb inside Devise.setup block
29
+ require 'devise-login-cookie'
30
+ config.warden do |manager|
31
+ manager.default_strategies(:scope => :user) << :devise_login_cookie
32
+ end
33
+
34
+
35
+ Background
36
+ ----------
37
+
38
+ The `devise-login-cookie` extension was born from a web application composed of Rails, PHP and Django. Because the components were on the same domain, Single Sign On could be implemented with a simple shared cookie.
39
+
40
+ While Devise can set a cookie for Remember Me logins, standard logins are only tracked in the session. Also, Devise cookies and Rails session cookies are tied to Ruby due to their reliance on `Marshal.dump`.
41
+
42
+ This extension sets a separate cookie upon authentication, signed in a cross-platform manner, and deletes it via the `before_logout` Warden hook. For the `:user` scope, the cookie is named `login_user_token`, consistent with `remember_user_token` from Devise's `rememberable`. The same cookie, if valid, triggers authentication.
43
+
44
+
45
+ Development and Tests
46
+ ---------------------
47
+
48
+ Patches are welcome; fork and send a pull request. Make sure the tests still work, and where possible, add to them. Let me know if you can't get the tests green to begin with.
49
+
50
+ RSpec 2 specifications cover the entire `Cookie` and part of the `Strategy`, but do not reach resource loading and authentication, nor cookie setting being triggered by login. These aspects need tobe tested in the host Rails application.
51
+
52
+
53
+ $ rake
54
+
55
+ DeviseLoginCookie::Cookie
56
+ #unset
57
+ deletes cookie
58
+ without any cookies
59
+ Cookie instance
60
+ should not be present
61
+ should not be valid
62
+ should not be set since 1970-01-01 10:00:00 +1000
63
+ #id
64
+ should be nil
65
+ #created_at
66
+ should be nil
67
+ #set
68
+ accepts resource
69
+ with an invalid cookie
70
+ Cookie instance
71
+ should be present
72
+ should not be valid
73
+ should not be set since 1970-01-01 10:00:00 +1000
74
+ #id
75
+ should be nil
76
+ #created_at
77
+ should be nil
78
+ #set
79
+ accepts resource
80
+ with a valid cookie
81
+ Cookie instance
82
+ should be present
83
+ should be valid
84
+ should not be set since 2010-12-08 23:46:30 +1100
85
+ should be set since 2010-12-08 23:46:29 +1100
86
+ should be set since 2010-12-08 23:46:28 +1100
87
+ #id
88
+ should == 5
89
+ #created_at
90
+ should == 2010-12-08 23:46:29 +1100
91
+ #set
92
+ accepts resource
93
+
94
+ DeviseLoginCookie::Strategy
95
+ #valid?
96
+ with no cookies
97
+ should not be valid
98
+ with invalid cookie
99
+ should not be valid
100
+ with valid cookie
101
+ should be valid
102
+
103
+ Finished in 0.06065 seconds
104
+ 24 examples, 0 failures
105
+
106
+
107
+ Credits
108
+ -------
109
+
110
+ (c) 2010 Paul Annesley; MIT Licence.
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
+
4
+ desc "Run all tests"
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ task :spec do
9
+ system 'bundle exec rspec --color --format documentation spec/*_spec.rb'
10
+ end
@@ -1,6 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "devise-login-cookie/version"
3
+ require "devise_login_cookie/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "devise-login-cookie"
@@ -19,7 +19,11 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
- s.add_dependency("signed_json")
23
- s.add_runtime_dependency("devise", ["~> 1.1"])
22
+ s.add_dependency("signed_json", ["~> 0.0.3"])
23
+ s.add_dependency("devise", ["~> 1.1"])
24
+
25
+ s.add_development_dependency("rspec", ["~> 2.2"])
26
+ s.add_development_dependency("rails") # devise requires this
27
+ s.add_development_dependency("rake")
24
28
 
25
29
  end
@@ -1,54 +1 @@
1
- module DeviseLoginCookie
2
-
3
- class Cookie
4
-
5
- def initialize(warden, scope)
6
- @scope = scope
7
- @warden = warden
8
- end
9
-
10
- def set(user)
11
- cookies[cookie_name] = cookie_options.merge(:value => cookie_value(user))
12
- end
13
-
14
- def unset
15
- cookies.delete cookie_name, cookie_options
16
- end
17
-
18
- private
19
-
20
- def cookies
21
- # .cookies provided by Devise in warden_compat.rb
22
- # Roughly equivalent to: ActionDispatch::Request.new(env).cookie_jar
23
- @warden.cookies
24
- end
25
-
26
- def cookie_name
27
- "login_#{@scope}_token"
28
- end
29
-
30
- def cookie_value(user)
31
- sign [ user.id, Time.now.to_i ]
32
- end
33
-
34
- def cookie_options
35
- Rails.configuration.session_options.slice(:path, :domain, :secure, :httponly)
36
- end
37
-
38
- def sign(input)
39
- require 'signed_json'
40
- signer = SignedJson::Signer.new(Rails.configuration.secret_token)
41
- signer.encode input
42
- end
43
-
44
- end
45
-
46
- Warden::Manager.after_set_user do |user, warden, options|
47
- Cookie.new(warden, options[:scope]).set(user)
48
- end
49
-
50
- Warden::Manager.before_logout do |user, warden, options|
51
- Cookie.new(warden, options[:scope]).unset
52
- end
53
-
54
- end
1
+ require "devise_login_cookie"
@@ -0,0 +1,12 @@
1
+ require "devise_login_cookie/cookie"
2
+ require "devise_login_cookie/strategy"
3
+
4
+ Warden::Strategies.add(:devise_login_cookie, DeviseLoginCookie::Strategy)
5
+
6
+ Warden::Manager.after_set_user do |user, warden, options|
7
+ DeviseLoginCookie::Cookie.new(warden.cookies, options[:scope]).set(user)
8
+ end
9
+
10
+ Warden::Manager.before_logout do |user, warden, options|
11
+ DeviseLoginCookie::Cookie.new(warden.cookies, options[:scope]).unset
12
+ end
@@ -0,0 +1,81 @@
1
+ require "active_support/core_ext/hash/slice"
2
+
3
+ module DeviseLoginCookie
4
+
5
+ class Cookie
6
+
7
+ # for non-Rails test environment.
8
+ attr_writer :session_options
9
+ attr_accessor :secret_token
10
+
11
+ def initialize(cookies, scope)
12
+ @cookies = cookies
13
+ @scope = scope
14
+ end
15
+
16
+ # Sets the cookie, referencing the given resource.id (e.g. User)
17
+ def set(resource)
18
+ @cookies[cookie_name] = cookie_options.merge(:value => encoded_value(resource))
19
+ end
20
+
21
+ # Unsets the cookie via the HTTP response.
22
+ def unset
23
+ @cookies.delete cookie_name, cookie_options
24
+ end
25
+
26
+ # The id of the resource (e.g. User) referenced in the cookie.
27
+ def id
28
+ value[0]
29
+ end
30
+
31
+ # The Time at which the cookie was created.
32
+ def created_at
33
+ valid? ? Time.at(value[1]) : nil
34
+ end
35
+
36
+ # Whether the cookie appears valid.
37
+ def valid?
38
+ present? && value.all?
39
+ end
40
+
41
+ def present?
42
+ @cookies[cookie_name].present?
43
+ end
44
+
45
+ # Whether the cookie was set since the given Time
46
+ def set_since?(time)
47
+ created_at && created_at >= time
48
+ end
49
+
50
+ private
51
+
52
+ def value
53
+ begin
54
+ @value = signer.decode @cookies[cookie_name]
55
+ rescue SignedJson::Error
56
+ [nil, nil]
57
+ end
58
+ end
59
+
60
+ def cookie_name
61
+ :"login_#{@scope}_token"
62
+ end
63
+
64
+ def encoded_value(resource)
65
+ signer.encode [ resource.id, Time.now.to_i ]
66
+ end
67
+
68
+ def cookie_options
69
+ @session_options ||= Rails.configuration.session_options
70
+ @session_options.slice(:path, :domain, :secure, :httponly)
71
+ end
72
+
73
+ def signer
74
+ require 'signed_json'
75
+ secret = secret_token || Rails.configuration.secret_token
76
+ @signer ||= SignedJson::Signer.new(secret)
77
+ end
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,45 @@
1
+ module DeviseLoginCookie
2
+
3
+ class Strategy < ::Devise::Strategies::Authenticatable
4
+
5
+ # TODO: configurable TTL
6
+ COOKIE_TTL = 86400 # one day
7
+
8
+ # for non-Rails test environment.
9
+ attr_accessor :secret_token
10
+
11
+ def valid?
12
+ cookie.valid?
13
+ end
14
+
15
+ def authenticate!
16
+ if fresh?(cookie) && validate(resource)
17
+ success!(resource)
18
+ else
19
+ pass
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def cookie
26
+ @cookie ||= Cookie.new(cookies, scope).tap do |cookie|
27
+ cookie.secret_token = secret_token if secret_token
28
+ end
29
+ end
30
+
31
+ def fresh?(cookie)
32
+ cookie.set_since?(Time.now - COOKIE_TTL)
33
+ end
34
+
35
+ def resource
36
+ @resource ||= mapping.to.find(cookie.id)
37
+ end
38
+
39
+ def pass
40
+ cookie.unset
41
+ super
42
+ end
43
+ end
44
+
45
+ end
@@ -1,3 +1,3 @@
1
1
  module DeviseLoginCookie
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,109 @@
1
+ require "spec_helper"
2
+
3
+ module DeviseLoginCookie
4
+
5
+ describe Cookie do
6
+
7
+ include DeviseLoginCookie::SpecHelpers
8
+
9
+ describe "#unset" do
10
+ it "deletes cookie" do
11
+ options = { :path => "/a", :domain => "a.bc", :secure => true, :httponly => true }
12
+ jar = double().tap do |jar|
13
+ jar.should_receive(:delete).with(:login_test_token, options)
14
+ end
15
+ Cookie.new(jar, :test).tap{ |c| c.session_options = options }.unset
16
+ end
17
+ end
18
+
19
+ describe "without any cookies" do
20
+ let(:cookie) { create_cookie }
21
+ subject { cookie }
22
+
23
+ describe "Cookie instance" do
24
+ it { should_not be_present }
25
+ it { should_not be_valid }
26
+ it { should_not be_set_since(Time.at(0)) }
27
+ end
28
+
29
+ describe "#id" do
30
+ subject { cookie.id }
31
+ it { should be_nil }
32
+ end
33
+
34
+ describe "#created_at" do
35
+ subject { cookie.created_at }
36
+ it { should be_nil }
37
+ end
38
+
39
+ describe "#set" do
40
+ it "accepts resource" do
41
+ cookie.set(resource(10))
42
+ cookie.id.should == 10
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "with an invalid cookie" do
48
+ let(:cookie) { create_cookie :login_test_token => "blarg" }
49
+ subject { cookie }
50
+
51
+ describe "Cookie instance" do
52
+ it { should be_present }
53
+ it { should_not be_valid }
54
+ it { should_not be_set_since(Time.at(0)) }
55
+ end
56
+
57
+ describe "#id" do
58
+ subject { cookie.id }
59
+ it { should be_nil }
60
+ end
61
+
62
+ describe "#created_at" do
63
+ subject { cookie.created_at }
64
+ it { should be_nil }
65
+ end
66
+
67
+ describe "#set" do
68
+ it "accepts resource" do
69
+ cookie.set(resource(10))
70
+ cookie.id.should == 10
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "with a valid cookie" do
76
+ # force integer precision, rather than float.
77
+ let(:now) { Time.at(Time.now.to_i) }
78
+ let(:cookie) { create_valid_cookie(5, now) }
79
+ subject { cookie }
80
+
81
+ describe "Cookie instance" do
82
+ it { should be_present }
83
+ it { should be_valid }
84
+ it { should_not be_set_since(now + 1) }
85
+ it { should be_set_since(now) }
86
+ it { should be_set_since(now - 1) }
87
+ end
88
+
89
+ describe "#id" do
90
+ subject { cookie.id }
91
+ it { should == 5 }
92
+ end
93
+
94
+ describe "#created_at" do
95
+ subject { cookie.created_at }
96
+ it { should == now }
97
+ end
98
+
99
+ describe "#set" do
100
+ it "accepts resource" do
101
+ cookie.set(resource(10))
102
+ cookie.id.should == 10
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,52 @@
1
+ require "rails"
2
+ require "devise"
3
+ require "devise_login_cookie"
4
+ require "action_dispatch/middleware/cookies"
5
+
6
+ module DeviseLoginCookie
7
+
8
+ module SpecHelpers
9
+
10
+ def resource(id)
11
+ require "ostruct"
12
+ OpenStruct.new(:id => id)
13
+ end
14
+
15
+ def cookie_jar(cookies = {})
16
+ ActionDispatch::Cookies::CookieJar.new.tap do |jar|
17
+ cookies.each { |key, value| jar[key] = value }
18
+ end
19
+ end
20
+
21
+ def create_cookie(cookies = {})
22
+ Cookie.new(cookie_jar(cookies), :test).tap do |cookie|
23
+ cookie.session_options = {}
24
+ cookie.secret_token = "secret"
25
+ end
26
+ end
27
+
28
+ def create_valid_cookie(id, created_at)
29
+ create_cookie :login_test_token => signed_cookie_value(id, created_at.to_i)
30
+ end
31
+
32
+ def signed_cookie_value(id, created_at)
33
+ # hacky shortcut better than re-implementing?
34
+ Cookie.new(nil, nil).tap do |cookie|
35
+ cookie.secret_token = "secret"
36
+ end.send(:signer).encode [id, created_at]
37
+ end
38
+
39
+ def create_strategy(cookies = {})
40
+ env = { "action_dispatch.cookies" => cookie_jar(cookies) }
41
+ Strategy.new(env, :test).tap do |strategy|
42
+ strategy.secret_token = "secret"
43
+ end
44
+ end
45
+
46
+ def create_valid_strategy
47
+ create_strategy :login_test_token => signed_cookie_value(1, Time.now.to_i)
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ module DeviseLoginCookie
4
+
5
+ describe Strategy do
6
+
7
+ include DeviseLoginCookie::SpecHelpers
8
+
9
+ describe "#valid?" do
10
+
11
+ describe "with no cookies" do
12
+ subject { create_strategy }
13
+ it { should_not be_valid }
14
+ end
15
+
16
+ describe "with invalid cookie" do
17
+ subject { create_strategy(:login_test_token => "blarg") }
18
+ it { should_not be_valid }
19
+ end
20
+
21
+ describe "with valid cookie" do
22
+ subject { create_valid_strategy }
23
+ it { should be_valid }
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 1
7
8
  - 0
8
- - 5
9
- version: 0.0.5
9
+ version: 0.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Paul Annesley
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-11-18 00:00:00 +11:00
17
+ date: 2010-12-09 00:00:00 +11:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -23,11 +23,13 @@ dependencies:
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
25
25
  requirements:
26
- - - ">="
26
+ - - ~>
27
27
  - !ruby/object:Gem::Version
28
28
  segments:
29
29
  - 0
30
- version: "0"
30
+ - 0
31
+ - 3
32
+ version: 0.0.3
31
33
  type: :runtime
32
34
  version_requirements: *id001
33
35
  - !ruby/object:Gem::Dependency
@@ -44,6 +46,46 @@ dependencies:
44
46
  version: "1.1"
45
47
  type: :runtime
46
48
  version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 2
59
+ - 2
60
+ version: "2.2"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: rails
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: rake
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ type: :development
88
+ version_requirements: *id005
47
89
  description: Devise sets a "remember_token" cookie for Remember Me logins, but not for standard logins. This extension sets a separate cookie on login, which makes sharing login state between same-domain web applications easier.
48
90
  email:
49
91
  - paul@annesley.cc
@@ -60,7 +102,13 @@ files:
60
102
  - Rakefile
61
103
  - devise-login-cookie.gemspec
62
104
  - lib/devise-login-cookie.rb
63
- - lib/devise-login-cookie/version.rb
105
+ - lib/devise_login_cookie.rb
106
+ - lib/devise_login_cookie/cookie.rb
107
+ - lib/devise_login_cookie/strategy.rb
108
+ - lib/devise_login_cookie/version.rb
109
+ - spec/cookie_spec.rb
110
+ - spec/spec_helper.rb
111
+ - spec/strategy_spec.rb
64
112
  has_rdoc: true
65
113
  homepage: http://rubygems.org/gems/devise-login-cookie
66
114
  licenses: []