omniauth-identity2 2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +13 -0
- data/Guardfile +10 -0
- data/LICENSE +21 -0
- data/README.markdown +240 -0
- data/Rakefile +9 -0
- data/lib/omniauth-identity.rb +2 -0
- data/lib/omniauth-identity/version.rb +5 -0
- data/lib/omniauth/identity.rb +18 -0
- data/lib/omniauth/identity/model.rb +116 -0
- data/lib/omniauth/identity/models/active_record.rb +24 -0
- data/lib/omniauth/identity/models/couch_potato.rb +31 -0
- data/lib/omniauth/identity/models/data_mapper.rb +32 -0
- data/lib/omniauth/identity/models/mongoid.rb +33 -0
- data/lib/omniauth/identity/secure_password.rb +78 -0
- data/lib/omniauth/strategies/identity.rb +103 -0
- data/omniauth-identity.gemspec +33 -0
- data/spec/omniauth/identity/model_spec.rb +121 -0
- data/spec/omniauth/identity/models/active_record_spec.rb +16 -0
- data/spec/omniauth/identity/models/couch_potato_spec.rb +16 -0
- data/spec/omniauth/identity/models/data_mapper_spec.rb +24 -0
- data/spec/omniauth/identity/models/mongoid_spec.rb +23 -0
- data/spec/omniauth/identity/secure_password_spec.rb +25 -0
- data/spec/omniauth/strategies/identity_spec.rb +141 -0
- data/spec/spec_helper.rb +20 -0
- metadata +264 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Identity
|
5
|
+
module Models
|
6
|
+
class ActiveRecord < ::ActiveRecord::Base
|
7
|
+
include OmniAuth::Identity::Model
|
8
|
+
include OmniAuth::Identity::SecurePassword
|
9
|
+
|
10
|
+
self.abstract_class = true
|
11
|
+
has_secure_password
|
12
|
+
|
13
|
+
def self.auth_key=(key)
|
14
|
+
super
|
15
|
+
validates_uniqueness_of key, :case_sensitive => false
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.locate(search_hash)
|
19
|
+
where(search_hash).first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'couch_potato'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Identity
|
5
|
+
module Models
|
6
|
+
# can not be named CouchPotato since there is a class with that name
|
7
|
+
module CouchPotatoModule
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
|
11
|
+
base.class_eval do
|
12
|
+
|
13
|
+
include ::OmniAuth::Identity::Model
|
14
|
+
include ::OmniAuth::Identity::SecurePassword
|
15
|
+
|
16
|
+
has_secure_password
|
17
|
+
|
18
|
+
def self.auth_key=(key)
|
19
|
+
super
|
20
|
+
validates_uniqueness_of key, :case_sensitive => false
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.locate(search_hash)
|
24
|
+
where(search_hash).first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'dm-validations'
|
3
|
+
|
4
|
+
module OmniAuth
|
5
|
+
module Identity
|
6
|
+
module Models
|
7
|
+
module DataMapper
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
include OmniAuth::Identity::Model
|
11
|
+
include OmniAuth::Identity::SecurePassword
|
12
|
+
|
13
|
+
# http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-persisted-3F
|
14
|
+
# http://rubydoc.info/github/mongoid/mongoid/master/Mongoid/State#persisted%3F-instance_method
|
15
|
+
alias persisted? valid?
|
16
|
+
|
17
|
+
has_secure_password
|
18
|
+
|
19
|
+
def self.auth_key=(key)
|
20
|
+
super
|
21
|
+
validates_uniqueness_of :key
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.locate(search_hash)
|
25
|
+
all(search_hash).first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end # DataMapper
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Identity
|
5
|
+
module Models
|
6
|
+
module Mongoid
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
|
10
|
+
base.class_eval do
|
11
|
+
|
12
|
+
include ::OmniAuth::Identity::Model
|
13
|
+
include ::OmniAuth::Identity::SecurePassword
|
14
|
+
|
15
|
+
has_secure_password
|
16
|
+
|
17
|
+
def self.auth_key=(key)
|
18
|
+
super
|
19
|
+
validates_uniqueness_of key, :case_sensitive => false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.locate(search_hash)
|
23
|
+
where(search_hash).first
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'bcrypt'
|
2
|
+
|
3
|
+
module OmniAuth
|
4
|
+
module Identity
|
5
|
+
# This is taken directly from Rails 3.1 code and is used if
|
6
|
+
# the version of ActiveModel that's being used does not
|
7
|
+
# include SecurePassword. The only difference is that instead of
|
8
|
+
# using ActiveSupport::Concern, it checks to see if there is already
|
9
|
+
# a has_secure_password method.
|
10
|
+
module SecurePassword
|
11
|
+
def self.included(base)
|
12
|
+
unless base.respond_to?(:has_secure_password)
|
13
|
+
base.extend ClassMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
# Adds methods to set and authenticate against a BCrypt password.
|
19
|
+
# This mechanism requires you to have a password_digest attribute.
|
20
|
+
#
|
21
|
+
# Validations for presence of password, confirmation of password (using
|
22
|
+
# a "password_confirmation" attribute) are automatically added.
|
23
|
+
# You can add more validations by hand if need be.
|
24
|
+
#
|
25
|
+
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
26
|
+
#
|
27
|
+
# # Schema: User(name:string, password_digest:string)
|
28
|
+
# class User < ActiveRecord::Base
|
29
|
+
# has_secure_password
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
|
33
|
+
# user.save # => false, password required
|
34
|
+
# user.password = "mUc3m00RsqyRe"
|
35
|
+
# user.save # => false, confirmation doesn't match
|
36
|
+
# user.password_confirmation = "mUc3m00RsqyRe"
|
37
|
+
# user.save # => true
|
38
|
+
# user.authenticate("notright") # => false
|
39
|
+
# user.authenticate("mUc3m00RsqyRe") # => user
|
40
|
+
# User.find_by_name("david").try(:authenticate, "notright") # => nil
|
41
|
+
# User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
|
42
|
+
def has_secure_password
|
43
|
+
attr_reader :password
|
44
|
+
|
45
|
+
validates_confirmation_of :password
|
46
|
+
validates_presence_of :password_digest
|
47
|
+
|
48
|
+
include InstanceMethodsOnActivation
|
49
|
+
|
50
|
+
if respond_to?(:attributes_protected_by_default)
|
51
|
+
def self.attributes_protected_by_default
|
52
|
+
super + ['password_digest']
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethodsOnActivation
|
59
|
+
# Returns self if the password is correct, otherwise false.
|
60
|
+
def authenticate(unencrypted_password)
|
61
|
+
if BCrypt::Password.new(password_digest) == unencrypted_password
|
62
|
+
self
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Encrypts the password into the password_digest attribute.
|
69
|
+
def password=(unencrypted_password)
|
70
|
+
@password = unencrypted_password
|
71
|
+
if unencrypted_password && !unencrypted_password.empty?
|
72
|
+
self.password_digest = BCrypt::Password.create(unencrypted_password)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module OmniAuth
|
2
|
+
module Strategies
|
3
|
+
# The identity strategy allows you to provide simple internal
|
4
|
+
# user authentication using the same process flow that you
|
5
|
+
# use for external OmniAuth providers.
|
6
|
+
class Identity
|
7
|
+
include OmniAuth::Strategy
|
8
|
+
|
9
|
+
option :fields, [:name, :email]
|
10
|
+
option :on_login, nil
|
11
|
+
option :on_registration, nil
|
12
|
+
option :on_failed_registration, nil
|
13
|
+
option :locate_conditions, lambda{|req| {model.auth_key => req['auth_key']} }
|
14
|
+
|
15
|
+
def request_phase
|
16
|
+
if options[:on_login]
|
17
|
+
options[:on_login].call(self.env)
|
18
|
+
else
|
19
|
+
OmniAuth::Form.build(
|
20
|
+
:title => (options[:title] || "Identity Verification"),
|
21
|
+
:url => callback_path
|
22
|
+
) do |f|
|
23
|
+
f.text_field 'Login', 'auth_key'
|
24
|
+
f.password_field 'Password', 'password'
|
25
|
+
f.html "<p align='center'><a href='#{registration_path}'>Create an Identity</a></p>"
|
26
|
+
end.to_response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def callback_phase
|
31
|
+
return fail!(:invalid_credentials) unless identity
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def other_phase
|
36
|
+
if on_registration_path?
|
37
|
+
if request.get?
|
38
|
+
registration_form
|
39
|
+
elsif request.post?
|
40
|
+
registration_phase
|
41
|
+
end
|
42
|
+
else
|
43
|
+
call_app!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def registration_form
|
48
|
+
if options[:on_registration]
|
49
|
+
options[:on_registration].call(self.env)
|
50
|
+
else
|
51
|
+
OmniAuth::Form.build(:title => 'Register Identity') do |f|
|
52
|
+
options[:fields].each do |field|
|
53
|
+
f.text_field field.to_s.capitalize, field.to_s
|
54
|
+
end
|
55
|
+
f.password_field 'Password', 'password'
|
56
|
+
f.password_field 'Confirm Password', 'password_confirmation'
|
57
|
+
end.to_response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def registration_phase
|
62
|
+
attributes = (options[:fields] + [:password, :password_confirmation]).inject({}){|h,k| h[k] = request[k.to_s]; h}
|
63
|
+
@identity = model.create(attributes)
|
64
|
+
if @identity.persisted?
|
65
|
+
env['PATH_INFO'] = callback_path
|
66
|
+
callback_phase
|
67
|
+
else
|
68
|
+
if options[:on_failed_registration]
|
69
|
+
self.env['omniauth.identity'] = @identity
|
70
|
+
options[:on_failed_registration].call(self.env)
|
71
|
+
else
|
72
|
+
registration_form
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
uid{ identity.uid }
|
78
|
+
info{ identity.info }
|
79
|
+
|
80
|
+
def registration_path
|
81
|
+
options[:registration_path] || "#{path_prefix}/#{name}/register"
|
82
|
+
end
|
83
|
+
|
84
|
+
def on_registration_path?
|
85
|
+
on_path?(registration_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def identity
|
89
|
+
if options.locate_conditions.is_a? Proc
|
90
|
+
conditions = instance_exec(request, &options.locate_conditions)
|
91
|
+
conditions.to_hash
|
92
|
+
else
|
93
|
+
conditions = options.locate_conditions.to_hash
|
94
|
+
end
|
95
|
+
@identity ||= model.authenticate(conditions, request['password'] )
|
96
|
+
end
|
97
|
+
|
98
|
+
def model
|
99
|
+
options[:model] || ::Identity
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/lib/omniauth-identity/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.add_runtime_dependency 'omniauth'
|
6
|
+
gem.add_runtime_dependency 'bcrypt'
|
7
|
+
|
8
|
+
gem.add_development_dependency 'maruku'
|
9
|
+
gem.add_development_dependency 'simplecov'
|
10
|
+
gem.add_development_dependency 'rack-test'
|
11
|
+
gem.add_development_dependency 'rake'
|
12
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
13
|
+
gem.add_development_dependency 'activerecord'
|
14
|
+
gem.add_development_dependency 'mongoid'
|
15
|
+
gem.add_development_dependency 'datamapper'
|
16
|
+
gem.add_development_dependency 'bson_ext'
|
17
|
+
gem.add_development_dependency 'byebug'
|
18
|
+
gem.add_development_dependency 'couch_potato'
|
19
|
+
|
20
|
+
gem.name = 'omniauth-identity2'
|
21
|
+
gem.version = OmniAuth::Identity::VERSION
|
22
|
+
gem.description = %q{Internal authentication handlers for OmniAuth. A modern version of the Omniauth Identity strategy.}
|
23
|
+
gem.summary = gem.description
|
24
|
+
gem.email = ['andy.roberts.uk@gmail.com']
|
25
|
+
gem.homepage = 'https://github.com/Jellybooks/omniauth-identity2'
|
26
|
+
gem.authors = ['Andrew Roberts', 'Michael Bleigh']
|
27
|
+
gem.license = 'MIT'
|
28
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
|
29
|
+
gem.files = `git ls-files`.split("\n")
|
30
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
|
+
gem.require_paths = ['lib']
|
32
|
+
gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if gem.respond_to? :required_rubygems_version=
|
33
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
class ExampleModel
|
2
|
+
include OmniAuth::Identity::Model
|
3
|
+
end
|
4
|
+
|
5
|
+
describe OmniAuth::Identity::Model do
|
6
|
+
context 'Class Methods' do
|
7
|
+
subject{ ExampleModel }
|
8
|
+
|
9
|
+
describe '.locate' do
|
10
|
+
it('should be abstract'){ expect{ subject.locate('abc') }.to raise_error(NotImplementedError) }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '.authenticate' do
|
14
|
+
it 'should call locate and then authenticate' do
|
15
|
+
mocked_instance = double('ExampleModel', :authenticate => 'abbadoo')
|
16
|
+
allow(subject).to receive(:locate).with('email' => 'example').and_return(mocked_instance)
|
17
|
+
expect(subject.authenticate({'email' => 'example'},'pass')).to eq('abbadoo')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should call locate with additional scopes when provided' do
|
21
|
+
mocked_instance = double('ExampleModel', :authenticate => 'abbadoo')
|
22
|
+
allow(subject).to receive(:locate).with('email' => 'example', 'user_type' => 'admin').and_return(mocked_instance)
|
23
|
+
expect(subject.authenticate({'email' => 'example', 'user_type' => 'admin'}, 'pass')).to eq('abbadoo')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should recover gracefully if locate is nil' do
|
27
|
+
allow(subject).to receive(:locate).and_return(nil)
|
28
|
+
expect(subject.authenticate('blah','foo')).to be false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'Instance Methods' do
|
34
|
+
subject{ ExampleModel.new }
|
35
|
+
|
36
|
+
describe '#authenticate' do
|
37
|
+
it('should be abstract'){ expect{ subject.authenticate('abc') }.to raise_error(NotImplementedError) }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#uid' do
|
41
|
+
it 'should default to #id' do
|
42
|
+
allow(subject).to receive(:respond_to?).with(:id).and_return(true)
|
43
|
+
allow(subject).to receive(:id).and_return 'wakka-do'
|
44
|
+
expect(subject.uid).to eq('wakka-do')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should stringify it' do
|
48
|
+
allow(subject).to receive(:id).and_return 123
|
49
|
+
expect(subject.uid).to eq('123')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should raise NotImplementedError if #id is not defined' do
|
53
|
+
allow(subject).to receive(:respond_to?).with(:id).and_return(false)
|
54
|
+
expect{ subject.uid }.to raise_error(NotImplementedError)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#auth_key' do
|
59
|
+
it 'should default to #email' do
|
60
|
+
allow(subject).to receive(:respond_to?).with(:email).and_return(true)
|
61
|
+
allow(subject).to receive(:email).and_return('bob@bob.com')
|
62
|
+
expect(subject.auth_key).to eq('bob@bob.com')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should use the class .auth_key' do
|
66
|
+
subject.class.auth_key 'login'
|
67
|
+
allow(subject).to receive(:login).and_return 'bob'
|
68
|
+
expect(subject.auth_key).to eq('bob')
|
69
|
+
subject.class.auth_key nil
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should raise a NotImplementedError if the auth_key method is not defined' do
|
73
|
+
expect{ subject.auth_key }.to raise_error(NotImplementedError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#auth_key=' do
|
78
|
+
it 'should default to setting email' do
|
79
|
+
allow(subject).to receive(:respond_to?).with(:email=).and_return(true)
|
80
|
+
expect(subject).to receive(:email=).with 'abc'
|
81
|
+
|
82
|
+
subject.auth_key = 'abc'
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should use a custom .auth_key if one is provided' do
|
86
|
+
subject.class.auth_key 'login'
|
87
|
+
allow(subject).to receive(:respond_to?).with(:login=).and_return(true)
|
88
|
+
expect(subject).to receive(:login=).with('abc')
|
89
|
+
|
90
|
+
subject.auth_key = 'abc'
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should raise a NotImplementedError if the autH_key method is not defined' do
|
94
|
+
expect{ subject.auth_key = 'broken' }.to raise_error(NotImplementedError)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#info' do
|
99
|
+
it 'should include attributes that are set' do
|
100
|
+
allow(subject).to receive(:name).and_return('Bob Bobson')
|
101
|
+
allow(subject).to receive(:nickname).and_return('bob')
|
102
|
+
|
103
|
+
expect(subject.info).to eq({
|
104
|
+
'name' => 'Bob Bobson',
|
105
|
+
'nickname' => 'bob'
|
106
|
+
})
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should automatically set name off of nickname' do
|
110
|
+
allow(subject).to receive(:nickname).and_return('bob')
|
111
|
+
subject.info['name'] == 'bob'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should not overwrite a provided name' do
|
115
|
+
allow(subject).to receive(:name).and_return('Awesome Dude')
|
116
|
+
allow(subject).to receive(:first_name).and_return('Frank')
|
117
|
+
expect(subject.info['name']).to eq('Awesome Dude')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|