obscured-doorman 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|