obscured-doorman 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +28 -0
  3. data/.github/dependabot.yml +11 -0
  4. data/.github/workflows/publish.yml +44 -0
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +14 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.simplecov +6 -0
  10. data/.travis.yml +17 -0
  11. data/CHANGELOG.md +31 -0
  12. data/Gemfile +8 -0
  13. data/Gemfile.lock +144 -0
  14. data/README.md +115 -0
  15. data/lib/obscured-doorman.rb +69 -0
  16. data/lib/obscured-doorman/base.rb +203 -0
  17. data/lib/obscured-doorman/configuration.rb +123 -0
  18. data/lib/obscured-doorman/errors.rb +44 -0
  19. data/lib/obscured-doorman/helpers.rb +66 -0
  20. data/lib/obscured-doorman/loggable.rb +51 -0
  21. data/lib/obscured-doorman/mailer.rb +46 -0
  22. data/lib/obscured-doorman/messages.rb +30 -0
  23. data/lib/obscured-doorman/models/token.rb +57 -0
  24. data/lib/obscured-doorman/models/user.rb +160 -0
  25. data/lib/obscured-doorman/providers/base/configuration.rb +69 -0
  26. data/lib/obscured-doorman/providers/bitbucket.rb +79 -0
  27. data/lib/obscured-doorman/providers/bitbucket/access_token.rb +27 -0
  28. data/lib/obscured-doorman/providers/bitbucket/configuration.rb +38 -0
  29. data/lib/obscured-doorman/providers/bitbucket/messages.rb +13 -0
  30. data/lib/obscured-doorman/providers/bitbucket/strategy.rb +53 -0
  31. data/lib/obscured-doorman/providers/github.rb +78 -0
  32. data/lib/obscured-doorman/providers/github/access_token.rb +23 -0
  33. data/lib/obscured-doorman/providers/github/configuration.rb +38 -0
  34. data/lib/obscured-doorman/providers/github/messages.rb +13 -0
  35. data/lib/obscured-doorman/providers/github/strategy.rb +53 -0
  36. data/lib/obscured-doorman/strategies/forgot_password.rb +157 -0
  37. data/lib/obscured-doorman/strategies/password.rb +38 -0
  38. data/lib/obscured-doorman/strategies/remember_me.rb +54 -0
  39. data/lib/obscured-doorman/utilities/roles.rb +11 -0
  40. data/lib/obscured-doorman/utilities/types.rb +14 -0
  41. data/lib/obscured-doorman/version.rb +7 -0
  42. data/obscured-doorman.gemspec +42 -0
  43. data/spec/config/mongoid.yml +11 -0
  44. data/spec/doorman_spec.rb +203 -0
  45. data/spec/errors_spec.rb +11 -0
  46. data/spec/factories/token_factory.rb +8 -0
  47. data/spec/factories/user_factory.rb +12 -0
  48. data/spec/helpers/application_helper.rb +52 -0
  49. data/spec/helpers/request_helper.rb +53 -0
  50. data/spec/loggable_spec.rb +27 -0
  51. data/spec/mailer_spec.rb +26 -0
  52. data/spec/matchers/time.rb +7 -0
  53. data/spec/setup.rb +58 -0
  54. data/spec/token_spec.rb +62 -0
  55. data/spec/user_spec.rb +151 -0
  56. 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