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.
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