obscured-doorman 0.4.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/.codeclimate.yml +28 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/publish.yml +44 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +14 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.simplecov +6 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +144 -0
- data/README.md +115 -0
- data/lib/obscured-doorman.rb +69 -0
- data/lib/obscured-doorman/base.rb +203 -0
- data/lib/obscured-doorman/configuration.rb +123 -0
- data/lib/obscured-doorman/errors.rb +44 -0
- data/lib/obscured-doorman/helpers.rb +66 -0
- data/lib/obscured-doorman/loggable.rb +51 -0
- data/lib/obscured-doorman/mailer.rb +46 -0
- data/lib/obscured-doorman/messages.rb +30 -0
- data/lib/obscured-doorman/models/token.rb +57 -0
- data/lib/obscured-doorman/models/user.rb +160 -0
- data/lib/obscured-doorman/providers/base/configuration.rb +69 -0
- data/lib/obscured-doorman/providers/bitbucket.rb +79 -0
- data/lib/obscured-doorman/providers/bitbucket/access_token.rb +27 -0
- data/lib/obscured-doorman/providers/bitbucket/configuration.rb +38 -0
- data/lib/obscured-doorman/providers/bitbucket/messages.rb +13 -0
- data/lib/obscured-doorman/providers/bitbucket/strategy.rb +53 -0
- data/lib/obscured-doorman/providers/github.rb +78 -0
- data/lib/obscured-doorman/providers/github/access_token.rb +23 -0
- data/lib/obscured-doorman/providers/github/configuration.rb +38 -0
- data/lib/obscured-doorman/providers/github/messages.rb +13 -0
- data/lib/obscured-doorman/providers/github/strategy.rb +53 -0
- data/lib/obscured-doorman/strategies/forgot_password.rb +157 -0
- data/lib/obscured-doorman/strategies/password.rb +38 -0
- data/lib/obscured-doorman/strategies/remember_me.rb +54 -0
- data/lib/obscured-doorman/utilities/roles.rb +11 -0
- data/lib/obscured-doorman/utilities/types.rb +14 -0
- data/lib/obscured-doorman/version.rb +7 -0
- data/obscured-doorman.gemspec +42 -0
- data/spec/config/mongoid.yml +11 -0
- data/spec/doorman_spec.rb +203 -0
- data/spec/errors_spec.rb +11 -0
- data/spec/factories/token_factory.rb +8 -0
- data/spec/factories/user_factory.rb +12 -0
- data/spec/helpers/application_helper.rb +52 -0
- data/spec/helpers/request_helper.rb +53 -0
- data/spec/loggable_spec.rb +27 -0
- data/spec/mailer_spec.rb +26 -0
- data/spec/matchers/time.rb +7 -0
- data/spec/setup.rb +58 -0
- data/spec/token_spec.rb +62 -0
- data/spec/user_spec.rb +151 -0
- metadata +361 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Providers
|
6
|
+
module Bitbucket
|
7
|
+
class AccessToken
|
8
|
+
attr_accessor :access_token
|
9
|
+
attr_accessor :refresh_token
|
10
|
+
attr_accessor :scopes
|
11
|
+
attr_accessor :expires_in
|
12
|
+
attr_accessor :expires_date
|
13
|
+
attr_accessor :emails
|
14
|
+
|
15
|
+
def initialize(attributes = {})
|
16
|
+
@access_token = attributes[:access_token]
|
17
|
+
@refresh_token = attributes[:refresh_token]
|
18
|
+
@scopes = attributes[:scopes]
|
19
|
+
@expires_in = attributes[:expires_in]
|
20
|
+
@expires_date = DateTime.now + expires_in.to_i.seconds
|
21
|
+
@emails = attributes[:emails]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../base/configuration', __dir__)
|
4
|
+
|
5
|
+
module Obscured
|
6
|
+
module Doorman
|
7
|
+
module Providers
|
8
|
+
module Bitbucket
|
9
|
+
class Configuration < Doorman::Providers::BaseConfiguration
|
10
|
+
def initialize
|
11
|
+
@config_values = {}
|
12
|
+
|
13
|
+
# set default attribute values
|
14
|
+
@defaults = _defaults
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def _defaults
|
20
|
+
OpenStruct.new(
|
21
|
+
provider: Doorman::Providers::Bitbucket,
|
22
|
+
enabled: false,
|
23
|
+
client_id: nil,
|
24
|
+
client_secret: nil,
|
25
|
+
scopes: 'account',
|
26
|
+
authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
|
27
|
+
token_url: 'https://bitbucket.org/site/oauth2/access_token',
|
28
|
+
login_url: '/doorman/oauth2/bitbucket',
|
29
|
+
redirect_url: '/doorman/oauth2/bitbucket/callback',
|
30
|
+
domains: [],
|
31
|
+
token: nil
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Providers
|
6
|
+
module Bitbucket
|
7
|
+
MESSAGES = {
|
8
|
+
invalid_domain: 'The domain associated with your email address is not whitelisted, please contact system administrator.'
|
9
|
+
}.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'haml'
|
4
|
+
require File.expand_path('messages', __dir__)
|
5
|
+
|
6
|
+
module Obscured
|
7
|
+
module Doorman
|
8
|
+
module Providers
|
9
|
+
module Bitbucket
|
10
|
+
class Strategy < Warden::Strategies::Base
|
11
|
+
def valid?
|
12
|
+
emails = Bitbucket.configuration[:token].emails
|
13
|
+
|
14
|
+
if Bitbucket.configuration[:domains].nil?
|
15
|
+
return true if emails.length.positive?
|
16
|
+
else
|
17
|
+
return true if valid_domain?
|
18
|
+
end
|
19
|
+
|
20
|
+
fail!(Bitbucket::MESSAGES[:invalid_domain])
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticate!
|
25
|
+
user = Doorman::User.where(:username.in => Bitbucket.configuration[:token].emails).first
|
26
|
+
|
27
|
+
if user.nil?
|
28
|
+
fail!(Doorman::MESSAGES[:login_bad_credentials])
|
29
|
+
elsif !user.confirmed
|
30
|
+
user.confirm
|
31
|
+
|
32
|
+
fail!(Doorman::MESSAGES[:login_not_confirmed])
|
33
|
+
else
|
34
|
+
success!(user)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def valid_domain?
|
41
|
+
emails = Bitbucket.configuration[:token].emails || []
|
42
|
+
domains = Bitbucket.configuration[:domains].split(',')
|
43
|
+
|
44
|
+
emails.each do |email|
|
45
|
+
return true unless domains.detect { |domain| email.end_with?(domain) }.nil?
|
46
|
+
end
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('github/configuration', __dir__)
|
4
|
+
require File.expand_path('github/messages', __dir__)
|
5
|
+
require File.expand_path('github/access_token', __dir__)
|
6
|
+
require File.expand_path('github/strategy', __dir__)
|
7
|
+
|
8
|
+
module Obscured
|
9
|
+
module Doorman
|
10
|
+
module Providers
|
11
|
+
module GitHub
|
12
|
+
class << self
|
13
|
+
# Configuration Object (instance of Obscured::Doorman::Providers::GitHub::Configuration)
|
14
|
+
attr_writer :configuration
|
15
|
+
|
16
|
+
def setup
|
17
|
+
yield(configuration)
|
18
|
+
end
|
19
|
+
|
20
|
+
def configuration
|
21
|
+
@configuration ||= GitHub::Configuration.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_configuration
|
25
|
+
configuration.defaults
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.registered(app)
|
30
|
+
app.helpers Doorman::Base::Helpers
|
31
|
+
app.helpers Doorman::Helpers
|
32
|
+
|
33
|
+
Warden::Strategies.add(:github, GitHub::Strategy)
|
34
|
+
|
35
|
+
app.get '/doorman/oauth2/github' do
|
36
|
+
redirect("#{GitHub.configuration[:authorize_url]}?client_id=#{GitHub.configuration[:client_id]}&response_type=code&scope=#{GitHub.configuration[:scopes]}")
|
37
|
+
end
|
38
|
+
|
39
|
+
app.get '/doorman/oauth2/github/callback/?' do
|
40
|
+
response = RestClient::Request.new(
|
41
|
+
method: :post,
|
42
|
+
url: GitHub.configuration[:token_url],
|
43
|
+
user: GitHub.configuration[:client_id],
|
44
|
+
password: GitHub.configuration[:client_secret],
|
45
|
+
payload: "code=#{params[:code]}&grant_type=authorization_code&scope=#{GitHub.configuration[:scopes]}",
|
46
|
+
headers: { Accept: 'application/json' }
|
47
|
+
).execute
|
48
|
+
|
49
|
+
json = JSON.parse(response.body)
|
50
|
+
token = GitHub::AccessToken.new(
|
51
|
+
access_token: json['access_token'],
|
52
|
+
token_type: json['token_type'],
|
53
|
+
scope: json['scope']
|
54
|
+
)
|
55
|
+
|
56
|
+
emails = RestClient.get 'https://api.github.com/user/emails', Authorization: "token #{token.access_token}"
|
57
|
+
emails = JSON.parse(emails.body)
|
58
|
+
token.emails = emails.map { |e| e['email'] }
|
59
|
+
GitHub.configuration[:token] = token
|
60
|
+
|
61
|
+
# Authenticate with :github strategy
|
62
|
+
warden.authenticate!(:github)
|
63
|
+
rescue RestClient::ExceptionWithResponse => e
|
64
|
+
message = JSON.parse(e.response)
|
65
|
+
Doorman.logger.error e
|
66
|
+
notify :error, "#{message['error_description']} (#{message['error']})"
|
67
|
+
redirect(Doorman.configuration.paths[:login])
|
68
|
+
ensure
|
69
|
+
# Notify if there are any messages from Warden.
|
70
|
+
notify :error, warden.message unless warden.message.blank?
|
71
|
+
|
72
|
+
redirect(Doorman.configuration.use_referrer && session[:return_to] ? session.delete(:return_to) : Doorman.configuration.paths[:success])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Providers
|
6
|
+
module GitHub
|
7
|
+
class AccessToken
|
8
|
+
attr_accessor :access_token
|
9
|
+
attr_accessor :token_type
|
10
|
+
attr_accessor :scope
|
11
|
+
attr_accessor :emails
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
@access_token = attributes[:access_token]
|
15
|
+
@token_type = attributes[:token_type]
|
16
|
+
@scopes = attributes[:scopes]
|
17
|
+
@emails = attributes[:emails]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../base/configuration', __dir__)
|
4
|
+
|
5
|
+
module Obscured
|
6
|
+
module Doorman
|
7
|
+
module Providers
|
8
|
+
module GitHub
|
9
|
+
class Configuration < Doorman::Providers::BaseConfiguration
|
10
|
+
def initialize
|
11
|
+
@config_values = {}
|
12
|
+
|
13
|
+
# set default attribute values
|
14
|
+
@defaults = _defaults
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def _defaults
|
20
|
+
OpenStruct.new(
|
21
|
+
provider: Doorman::Providers::GitHub,
|
22
|
+
enabled: false,
|
23
|
+
client_id: nil,
|
24
|
+
client_secret: nil,
|
25
|
+
scopes: 'user:email',
|
26
|
+
authorize_url: 'https://github.com/login/oauth/authorize',
|
27
|
+
token_url: 'https://github.com/login/oauth/access_token',
|
28
|
+
login_url: '/doorman/oauth2/github',
|
29
|
+
redirect_url: '/doorman/oauth2/github/callback',
|
30
|
+
domains: [],
|
31
|
+
token: nil
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Providers
|
6
|
+
module GitHub
|
7
|
+
MESSAGES = {
|
8
|
+
invalid_domain: 'The domain associated with your email address is not whitelisted, please contact system administrator.'
|
9
|
+
}.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'haml'
|
4
|
+
require File.expand_path('messages', __dir__)
|
5
|
+
|
6
|
+
module Obscured
|
7
|
+
module Doorman
|
8
|
+
module Providers
|
9
|
+
module GitHub
|
10
|
+
class Strategy < Warden::Strategies::Base
|
11
|
+
def valid?
|
12
|
+
emails = GitHub.configuration[:token].emails
|
13
|
+
|
14
|
+
if GitHub.configuration[:domains].nil?
|
15
|
+
return true if emails.length.positive?
|
16
|
+
else
|
17
|
+
return true if valid_domain?
|
18
|
+
end
|
19
|
+
|
20
|
+
fail!(GitHub::MESSAGES[:invalid_domain])
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def authenticate!
|
25
|
+
user = Doorman::User.where(:username.in => GitHub.configuration[:token].emails).first
|
26
|
+
|
27
|
+
if user.nil?
|
28
|
+
fail!(Doorman::MESSAGES[:login_bad_credentials])
|
29
|
+
elsif !user.confirmed
|
30
|
+
user.confirm
|
31
|
+
|
32
|
+
fail!(Doorman::MESSAGES[:login_not_confirmed])
|
33
|
+
else
|
34
|
+
success!(user)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def valid_domain?
|
41
|
+
emails = GitHub.configuration[:token].emails || []
|
42
|
+
domains = GitHub.configuration[:domains].split(',')
|
43
|
+
|
44
|
+
emails.each do |email|
|
45
|
+
return true unless domains.detect { |domain| email.end_with?(domain) }.nil?
|
46
|
+
end
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Strategies
|
6
|
+
module ForgotPassword
|
7
|
+
def self.registered(app)
|
8
|
+
Warden::Manager.after_authentication do |user, auth, _opts|
|
9
|
+
# If the user requested a new password,
|
10
|
+
# but then remembers and logs in,
|
11
|
+
# then invalidate password reset token
|
12
|
+
user.remembered_password! if auth.winning_strategy.is_a?(Doorman::Strategies::Password)
|
13
|
+
end
|
14
|
+
|
15
|
+
app.get '/doorman/forgot/?' do
|
16
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
17
|
+
|
18
|
+
email = cookies[:email]
|
19
|
+
email = params[:email] if email.nil?
|
20
|
+
|
21
|
+
haml :forgot, locals: { email: email }
|
22
|
+
end
|
23
|
+
|
24
|
+
app.post '/doorman/forgot' do
|
25
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
26
|
+
redirect(Doorman.configuration.paths[:login]) unless params[:user]
|
27
|
+
|
28
|
+
user = User.where(username: params[:user][:username]).first
|
29
|
+
if user.nil?
|
30
|
+
notify :error, :forgot_no_user
|
31
|
+
redirect(back)
|
32
|
+
else
|
33
|
+
if user.role.to_sym == Doorman::Roles::SYSTEM
|
34
|
+
notify :error, :reset_system_user
|
35
|
+
redirect(Doorman.configuration.paths[:forgot])
|
36
|
+
end
|
37
|
+
|
38
|
+
token = user.forgot_password!
|
39
|
+
if token.nil? && !token&.type.eql?(:password)
|
40
|
+
notify :error, :token_not_found
|
41
|
+
redirect(back)
|
42
|
+
end
|
43
|
+
if token&.used?
|
44
|
+
notify :error, :token_used
|
45
|
+
redirect(back)
|
46
|
+
end
|
47
|
+
|
48
|
+
if File.exist?('views/doorman/templates/password_reset.haml')
|
49
|
+
template = haml :'/templates/password_reset', layout: false, locals: {
|
50
|
+
user: user.username,
|
51
|
+
link: token_link('reset', token.token)
|
52
|
+
}
|
53
|
+
Doorman::Mailer.new(
|
54
|
+
to: user.username,
|
55
|
+
subject: 'Password change request',
|
56
|
+
text: "We have received a password change request for your account (#{user.username}). " + token_link('reset', token.token),
|
57
|
+
html: template
|
58
|
+
).deliver!
|
59
|
+
else
|
60
|
+
Doorman.logger.warn "Template not found (views/doorman/templates/password_reset.haml), account password reset at #{token_link('reset', token.token)}"
|
61
|
+
end
|
62
|
+
|
63
|
+
notify :success, :forgot_success
|
64
|
+
redirect(Doorman.configuration.paths[:login])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
app.get '/doorman/reset/:token/?' do
|
69
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
70
|
+
|
71
|
+
if params[:token].nil? || params[:token].empty?
|
72
|
+
notify :error, :token_not_found
|
73
|
+
redirect(Doorman.configuration.paths[:login])
|
74
|
+
end
|
75
|
+
|
76
|
+
token = Token.where(token: params[:token]).first
|
77
|
+
if token.nil?
|
78
|
+
notify :error, :token_not_found
|
79
|
+
redirect(Doorman.configuration.paths[:login])
|
80
|
+
end
|
81
|
+
if token&.used?
|
82
|
+
notify :error, :token_used
|
83
|
+
redirect(Doorman.configuration.paths[:login])
|
84
|
+
end
|
85
|
+
|
86
|
+
user = token&.user
|
87
|
+
if user.nil?
|
88
|
+
notify :error, :reset_no_user
|
89
|
+
redirect(Doorman.configuration.paths[:login])
|
90
|
+
end
|
91
|
+
|
92
|
+
haml :reset, locals: { token: token.token, email: user&.username }
|
93
|
+
end
|
94
|
+
|
95
|
+
app.post '/doorman/reset' do
|
96
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
97
|
+
redirect(Doorman.configuration.paths[:login]) unless params[:user]
|
98
|
+
|
99
|
+
token = Token.where(token: params[:user][:token]).first
|
100
|
+
if token.nil?
|
101
|
+
notify :error, :token_not_found
|
102
|
+
redirect(back)
|
103
|
+
end
|
104
|
+
if token&.used?
|
105
|
+
notify :error, :token_used
|
106
|
+
redirect(back)
|
107
|
+
end
|
108
|
+
|
109
|
+
user = token&.user
|
110
|
+
if user.nil?
|
111
|
+
notify :error, :reset_no_user
|
112
|
+
redirect(Doorman.configuration.paths[:login])
|
113
|
+
end
|
114
|
+
|
115
|
+
if user&.role&.to_sym == Doorman::Roles::SYSTEM
|
116
|
+
notify :error, :reset_system_user
|
117
|
+
redirect(Doorman.configuration.paths[:login])
|
118
|
+
end
|
119
|
+
|
120
|
+
success = user&.reset_password!(
|
121
|
+
params[:user][:password],
|
122
|
+
params[:user][:token]
|
123
|
+
)
|
124
|
+
|
125
|
+
if success && File.exist?('views/doorman/templates/password_confirmation.haml')
|
126
|
+
position = Geocoder.search(request.ip)
|
127
|
+
template = haml :'/templates/password_confirmation', layout: false, locals: {
|
128
|
+
user: user&.username,
|
129
|
+
browser: "#{request&.browser} #{request&.browser_version}",
|
130
|
+
location: "#{position&.first&.city},#{position&.first&.country}",
|
131
|
+
ip: request&.ip,
|
132
|
+
system: "#{request&.os} #{request&.os_version}"
|
133
|
+
}
|
134
|
+
Doorman::Mailer.new(
|
135
|
+
to: user&.username,
|
136
|
+
subject: 'Password change confirmation',
|
137
|
+
text: "The password for your account (#{user&.username}) was recently changed. This change was made from the following device or browser from: ",
|
138
|
+
html: template
|
139
|
+
).deliver!
|
140
|
+
else
|
141
|
+
Doorman.logger.warn "Template not found (views/doorman/templates/password_confirmation.haml) The password for your account (#{user&.username}) was recently changed."
|
142
|
+
|
143
|
+
notify :error, :reset_unmatched_passwords
|
144
|
+
redirect(Doorman.configuration.paths[:login])
|
145
|
+
end
|
146
|
+
|
147
|
+
user&.confirm!
|
148
|
+
warden.set_user(user)
|
149
|
+
notify :success, :reset_success
|
150
|
+
|
151
|
+
redirect(Doorman.configuration.use_referrer && session[:return_to] ? session.delete(:return_to) : Doorman.configuration.paths[:success])
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|