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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ # Contains logging behavior.
6
+ module Loggable
7
+ # Get the logger.
8
+ #
9
+ # @note Will try to grab Rails' logger first before creating a new logger
10
+ # with stdout.
11
+ #
12
+ # @example Get the logger.
13
+ # Loggable.logger
14
+ #
15
+ # @return [ Logger ] The logger.
16
+ def logger
17
+ return @logger if defined?(@logger)
18
+
19
+ @logger = default_logger
20
+ end
21
+
22
+ # Set the logger.
23
+ #
24
+ # @example Set the logger.
25
+ # Loggable.logger = Logger.new($stdout)
26
+ #
27
+ # @param [ Logger ] logger The logger to set.
28
+ #
29
+ # @return [ Logger ] The new logger.
30
+ def logger=(logger)
31
+ @logger = logger
32
+ end
33
+
34
+ private
35
+
36
+ # Gets the default Mongoid logger - stdout.
37
+ #
38
+ # @api private
39
+ #
40
+ # @example Get the default logger.
41
+ # Loggable.default_logger
42
+ #
43
+ # @return [ Logger ] The default logger.
44
+ def default_logger
45
+ logger = Logger.new($stdout)
46
+ logger.level = Doorman.configuration.log_level
47
+ logger
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ class Mailer
6
+ def initialize(opts = {})
7
+ @to = opts[:to]
8
+ @from = "doorman@#{Doorman.configuration.smtp_domain}"
9
+ @subject = opts[:subject]
10
+
11
+ @text = opts[:text]
12
+ @html = opts[:html]
13
+ end
14
+
15
+ def deliver!
16
+ Doorman.logger.debug "Sending mail to #{@to}, from: #{@from}, with subject: #{@subject} and text #{@text}"
17
+ mail = Mail.new(to: @to, from: @from, subject: @subject) do
18
+ delivery_method :smtp,
19
+ address: Doorman.configuration.smtp_server,
20
+ port: Doorman.configuration.smtp_port,
21
+ domain: Doorman.configuration.smtp_domain,
22
+ enable_starttls_auto: true,
23
+ authentication: :plain,
24
+ user_name: Doorman.configuration.smtp_username,
25
+ password: Doorman.configuration.smtp_password
26
+ end
27
+
28
+ unless @text.blank?
29
+ text_part = Mail::Part.new(body: @text)
30
+ mail.text_part = text_part
31
+ end
32
+
33
+ unless @html.blank?
34
+ html_part = Mail::Part.new(body: @html) do
35
+ content_type 'text/html; charset=utf-8'
36
+ end
37
+ mail.html_part = html_part
38
+ end
39
+
40
+ mail.deliver
41
+ rescue => e
42
+ Doorman.logger.error e
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ MESSAGES = {
6
+ auth_required: 'You must be logged in to view this page.',
7
+ signup_disabled: 'Registration is disabled, contact team member for creation of account!',
8
+ signup_success: 'You have signed up successfully. A confirmation email has been sent to you.',
9
+ confirm_no_user: 'Invalid confirmation URL. Please make sure you have the correct link from the email, and are not already confirmed.',
10
+ confirm_success: 'You have successfully confirmed your account. Please log in.',
11
+ # Auto login upon confirmation?
12
+ login_bad_credentials: 'Invalid Login and Password. Please try again.',
13
+ login_not_confirmed: 'You must confirm your account before you can log in. Please click the confirmation link sent to you.',
14
+ # Note: resend confirmation link?
15
+ logout_success: 'You have been logged out.',
16
+ forgot_no_user: 'There is no user with that Username or Email. Please try again.',
17
+ forgot_success: 'An email with instructions to reset your password has been sent to you.',
18
+ reset_no_user: 'Invalid reset URL. Please make sure you have the correct link from the email, and have already reset the password.',
19
+ reset_system_user: 'Your trying to reset the password of a system user, unfortunate for you, this action is not allowed',
20
+ reset_unmatched_passwords: 'Password and confirmation do not match. Please try again.',
21
+ reset_success: 'Your password has been reset.',
22
+ # Registration
23
+ register_account_exists: 'Account already registered.',
24
+ # Token
25
+ token_used: 'The token has already been used, request a new token and try again.',
26
+ token_expired: 'The token has expired, request a new token and try again.',
27
+ token_not_found: 'The token was not found, request a new token and try again.'
28
+ }.freeze
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ class Token
6
+ include Mongoid::Document
7
+ include Mongoid::Timestamps
8
+
9
+ store_in database: Doorman.configuration.db_name,
10
+ client: Doorman.configuration.db_client,
11
+ collection: 'tokens'
12
+
13
+ field :type, type: Symbol
14
+ field :token, type: String
15
+ field :expires_at, type: DateTime, default: -> { DateTime.now + 2.hours }
16
+ field :used_at, type: DateTime
17
+ field :user_id, type: BSON::ObjectId
18
+
19
+ belongs_to :user, autosave: true, class_name: 'Obscured::Doorman::User', inverse_of: 'tokens'
20
+
21
+ index({ expires_at: 1 }, background: true, expire_after_seconds: 172_800)
22
+ index({ used_at: 1 }, background: true, expire_after_seconds: 345_600)
23
+
24
+ class << self
25
+ def make(opts)
26
+ raise Doorman::Error.new(:already_exists, what: 'Token does already exists!') if Token.where(user: opts[:user], type: opts[:type]).exists?
27
+
28
+ token = new
29
+ token.user = opts[:user]
30
+ token.type = opts[:type]
31
+ token.token = opts[:token]
32
+ token.expires_at = opts[:expires] if opts[:expires]
33
+ token
34
+ end
35
+
36
+ def make!(opts)
37
+ token = make(opts)
38
+ token.save
39
+ token
40
+ end
41
+ end
42
+
43
+ def use!
44
+ self.used_at = DateTime.now
45
+ save
46
+ end
47
+
48
+ def usable?
49
+ used_at.nil? && expires_at > DateTime.now
50
+ end
51
+
52
+ def used?
53
+ !used_at.nil?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'obscured-timeline'
4
+
5
+ module Obscured
6
+ module Doorman
7
+ class User
8
+ include Mongoid::Document
9
+ include Mongoid::Timestamps
10
+ include Mongoid::Timeline::Tracker
11
+
12
+ store_in database: Doorman.configuration.db_name,
13
+ client: Doorman.configuration.db_client,
14
+ collection: 'users'
15
+
16
+ field :username, type: String
17
+ field :password, type: String
18
+ field :salt, type: String
19
+ field :first_name, type: String
20
+ field :last_name, type: String
21
+ field :mobile, type: String
22
+ field :role, type: Symbol, default: Doorman::Roles::ADMIN
23
+ field :confirmed, type: Boolean, default: false
24
+
25
+ has_many :tokens, autosave: true, class_name: 'Obscured::Doorman::Token', foreign_key: 'user_id'
26
+
27
+ index({ username: 1 }, background: true)
28
+
29
+ after_initialize :set_salt
30
+
31
+ alias email username
32
+
33
+ attr_accessor :confirmed
34
+
35
+ class << self
36
+ def make(opts)
37
+ raise Doorman::Error.new(:already_exists, what: 'User does already exists!') if User.where(username: opts[:username]).exists?
38
+
39
+ user = new
40
+ user.username = opts[:username]
41
+ user.set_password(opts[:password])
42
+ user.first_name = opts[:first_name] unless opts[:first_name].nil?
43
+ user.last_name = opts[:last_name] unless opts[:last_name].nil?
44
+ user.mobile = opts[:mobile] unless opts[:mobile].nil?
45
+ user.role = opts[:role] unless opts[:role].nil?
46
+ user.confirmed = opts[:confirmed] unless opts[:confirmed].nil?
47
+ user.add_event(type: :account, message: 'Account created', producer: opts[:producer].nil? ? user.username : opts[:producer])
48
+ user
49
+ end
50
+
51
+ def make!(opts)
52
+ user = make(opts)
53
+ user.save
54
+ user
55
+ end
56
+
57
+ def authenticate(username, password)
58
+ user = where(username: username).first
59
+ return user if user&.authenticated?(password)
60
+
61
+ nil
62
+ end
63
+
64
+ def registered?(username)
65
+ where(username: username).exists?
66
+ end
67
+ end
68
+
69
+ def name
70
+ "#{first_name} #{last_name}"
71
+ end
72
+
73
+ def name=(arguments)
74
+ self.first_name = arguments[:first_name]
75
+ self.last_name = arguments[:last_name]
76
+ end
77
+
78
+ def set_password(password)
79
+ self.password = BCrypt::Password.create(password)
80
+ end
81
+
82
+ def authenticated?(password)
83
+ (BCrypt::Password.new(self.password) == password)
84
+ end
85
+ alias password? authenticated?
86
+
87
+ def remember_me!
88
+ add_event(type: :remember, message: 'Account set to be remembered upon login', producer: username)
89
+ token = tokens.build(
90
+ type: :remember,
91
+ token: SecureRandom.uuid,
92
+ expires_at: (DateTime.now + Doorman.configuration.remember_for.days)
93
+ )
94
+ save
95
+ token
96
+ end
97
+
98
+ def forget_me!
99
+ add_event(type: :remember, message: 'Account set not to be remembered upon login', producer: username)
100
+ tokens.where(type: :remember).destroy
101
+ end
102
+
103
+ def confirm
104
+ add_event(type: :confirm, message: 'Confirmation token created', producer: username)
105
+ tokens.where(type: :confirm).destroy
106
+ token = tokens.build(
107
+ type: :confirm,
108
+ token: SecureRandom.uuid,
109
+ expires_at: (DateTime.now + 14.days)
110
+ )
111
+ save
112
+ token
113
+ end
114
+
115
+ def confirm!
116
+ add_event(type: :confirmation, message: 'Account was successfully confirmed', producer: username)
117
+ self.confirmed = true
118
+ tokens.where(type: :confirm).destroy
119
+ save
120
+ end
121
+
122
+ def forgot_password!
123
+ add_event(type: :password, message: 'Reset password procedure has been started', producer: username)
124
+ tokens.where(type: :password).destroy
125
+ token = tokens.build(
126
+ user: self,
127
+ type: :password,
128
+ token: SecureRandom.uuid,
129
+ expires_at: (DateTime.now + 2.hours)
130
+ )
131
+ save
132
+ token
133
+ end
134
+
135
+ def remembered_password!
136
+ add_event(type: :password, message: 'Reset password procedure has been cancelled since successful login was achieved', producer: username)
137
+ tokens.where(type: :password).destroy
138
+ end
139
+
140
+ def reset_password!(password, token)
141
+ token = tokens.find_by(token: token)
142
+ if token && token.type.eql?(:password)
143
+ set_password(password)
144
+ add_event(type: :password, message: 'Password was successfully reset', producer: username)
145
+ return save
146
+ end
147
+ false
148
+ end
149
+
150
+ protected
151
+
152
+ def set_salt
153
+ return unless salt.nil? || salt.empty?
154
+
155
+ secret = Digest::SHA1.hexdigest("--#{username}--")
156
+ self.salt = Digest::SHA1.hexdigest("--#{Time.now.utc}--#{secret}--")
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Obscured
4
+ module Doorman
5
+ module Providers
6
+ class BaseConfiguration
7
+ def self.config_option(name)
8
+ define_method(name) do
9
+ read_value(name)
10
+ end
11
+
12
+ define_method("#{name}=") do |value|
13
+ set_value(name, value)
14
+ end
15
+ end
16
+
17
+ # Name of the authentication provider
18
+ config_option :provider
19
+ # Enables/disables the provider
20
+ config_option :enabled
21
+
22
+ # Provider client id
23
+ config_option :client_id
24
+ # Provider client secret
25
+ config_option :client_secret
26
+ # Provider scopes
27
+ config_option :scopes
28
+
29
+ # Provider authentication endpoint
30
+ config_option :authorize_url
31
+ # Provider token endpoint
32
+ config_option :token_url
33
+ # Provider login endpoint
34
+ config_option :login_url
35
+ # Provider redirect endpoint
36
+ config_option :redirect_url
37
+
38
+ # Authentication domains to login
39
+ config_option :domains
40
+ # Authentication token
41
+ config_option :token
42
+
43
+ attr_reader :defaults
44
+
45
+ def [](key)
46
+ read_value(key)
47
+ end
48
+
49
+ def []=(key, value)
50
+ set_value(key, value)
51
+ end
52
+
53
+ private
54
+
55
+ def read_value(name)
56
+ if @config_values.key?(name)
57
+ @config_values[name]
58
+ else
59
+ @defaults.send(name)
60
+ end
61
+ end
62
+
63
+ def set_value(name, value)
64
+ @config_values[name] = value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('bitbucket/configuration', __dir__)
4
+ require File.expand_path('bitbucket/messages', __dir__)
5
+ require File.expand_path('bitbucket/access_token', __dir__)
6
+ require File.expand_path('bitbucket/strategy', __dir__)
7
+
8
+ module Obscured
9
+ module Doorman
10
+ module Providers
11
+ module Bitbucket
12
+ class << self
13
+ # Configuration Object (instance of Obscured::Doorman::Providers::Bitbucket::Configuration)
14
+ attr_writer :configuration
15
+
16
+ def setup
17
+ yield(configuration)
18
+ end
19
+
20
+ def configuration
21
+ @configuration ||= Bitbucket::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(:bitbucket, Bitbucket::Strategy)
34
+
35
+ app.get '/doorman/oauth2/bitbucket' do
36
+ redirect("#{Bitbucket.configuration[:authorize_url]}?client_id=#{Bitbucket.configuration[:client_id]}&response_type=code&scopes=#{Bitbucket.configuration[:scopes]}")
37
+ end
38
+
39
+ app.get '/doorman/oauth2/bitbucket/callback/?' do
40
+ response = RestClient::Request.new(
41
+ method: :post,
42
+ url: Bitbucket.configuration[:token_url],
43
+ user: Bitbucket.configuration[:client_id],
44
+ password: Bitbucket.configuration[:client_secret],
45
+ payload: "code=#{params[:code]}&grant_type=authorization_code&scope=#{Bitbucket.configuration[:scopes]}",
46
+ headers: { Accept: 'application/json' }
47
+ ).execute
48
+
49
+ json = JSON.parse(response.body)
50
+ token = Bitbucket::AccessToken.new(
51
+ access_token: json['access_token'],
52
+ refresh_token: json['refresh_token'],
53
+ scopes: json['scopes'],
54
+ expires_in: json['expires_in']
55
+ )
56
+
57
+ emails = RestClient.get 'https://api.bitbucket.org/2.0/user/emails', Authorization: "Bearer #{token.access_token}"
58
+ emails = JSON.parse(emails.body)
59
+ token.emails = emails.values[1].map { |e| e['email'] }
60
+ Bitbucket.configuration[:token] = token
61
+
62
+ # Authenticate with :bitbucket strategy
63
+ warden.authenticate!(:bitbucket)
64
+ rescue RestClient::ExceptionWithResponse => e
65
+ message = JSON.parse(e.response)
66
+ Doorman.logger.error e
67
+ notify :error, "#{message['error_description']} (#{message['error']})"
68
+ redirect(Doorman.configuration.paths[:login])
69
+ ensure
70
+ # Notify if there are any messages from Warden.
71
+ notify :error, warden.message unless warden.message.blank?
72
+
73
+ redirect(Doorman.configuration.use_referrer && session[:return_to] ? session.delete(:return_to) : Doorman.configuration.paths[:success])
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end