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