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.
- 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,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
require 'geocoder'
|
5
|
+
require 'haml'
|
6
|
+
require 'mail'
|
7
|
+
require 'mongoid'
|
8
|
+
require 'sinatra'
|
9
|
+
require 'sinatra/contrib'
|
10
|
+
require 'sinatra/flash'
|
11
|
+
require 'sinatra/partial'
|
12
|
+
require 'rack'
|
13
|
+
require 'rack/contrib'
|
14
|
+
require 'rack/contrib/cookies'
|
15
|
+
require 'rest-client'
|
16
|
+
require 'warden'
|
17
|
+
|
18
|
+
require 'obscured-doorman/loggable'
|
19
|
+
require 'obscured-doorman/configuration'
|
20
|
+
require 'obscured-doorman/errors'
|
21
|
+
require 'obscured-doorman/providers/bitbucket'
|
22
|
+
require 'obscured-doorman/providers/github'
|
23
|
+
require 'obscured-doorman/strategies/password'
|
24
|
+
require 'obscured-doorman/strategies/forgot_password'
|
25
|
+
require 'obscured-doorman/strategies/remember_me'
|
26
|
+
require 'obscured-doorman/utilities/roles'
|
27
|
+
require 'obscured-doorman/utilities/types'
|
28
|
+
require 'obscured-doorman/helpers'
|
29
|
+
require 'obscured-doorman/mailer'
|
30
|
+
require 'obscured-doorman/messages'
|
31
|
+
require 'obscured-doorman/version'
|
32
|
+
require 'obscured-doorman/base'
|
33
|
+
|
34
|
+
module Obscured
|
35
|
+
module Doorman
|
36
|
+
extend Loggable
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# Configuration Object (instance of Obscured::Doorman::Configuration)
|
40
|
+
attr_writer :configuration
|
41
|
+
|
42
|
+
##
|
43
|
+
# Configuration options should be set by passing a hash:
|
44
|
+
#
|
45
|
+
# Obscured::Doorman.setup do |cfg|
|
46
|
+
# cfg.confirmation = false,
|
47
|
+
# cfg.registration = true,
|
48
|
+
# cfg.smtp_domain = 'domain.tld',
|
49
|
+
# cfg.smtp_username = 'username',
|
50
|
+
# cfg.smtp_password = 'password',
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
def setup
|
54
|
+
yield(configuration)
|
55
|
+
|
56
|
+
require 'obscured-doorman/models/user'
|
57
|
+
require 'obscured-doorman/models/token'
|
58
|
+
end
|
59
|
+
|
60
|
+
def configuration
|
61
|
+
@configuration ||= Configuration.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def default_configuration
|
65
|
+
configuration.defaults
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
class Warden::SessionSerializer
|
6
|
+
def serialize(user)
|
7
|
+
user.id
|
8
|
+
end
|
9
|
+
|
10
|
+
def deserialize(id)
|
11
|
+
User.find(id)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Base
|
16
|
+
module Helpers
|
17
|
+
# Generates a flash message by trying to fetch a default message,
|
18
|
+
# if that fails just pass the message
|
19
|
+
def notify(type, message)
|
20
|
+
message = Doorman::MESSAGES[message] if message.is_a?(Symbol)
|
21
|
+
flash[type] = message
|
22
|
+
end
|
23
|
+
|
24
|
+
# Generates a url for confirm account or reset password
|
25
|
+
def token_link(action, token)
|
26
|
+
"http://#{env['HTTP_HOST']}/doorman/#{action}/#{token}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.registered(app)
|
31
|
+
app.helpers Doorman::Base::Helpers
|
32
|
+
app.helpers Doorman::Helpers
|
33
|
+
|
34
|
+
# Enable Sessions
|
35
|
+
app.set :sessions, true unless defined?(Rack::Session::Cookie)
|
36
|
+
|
37
|
+
app.use Warden::Manager do |config|
|
38
|
+
config.scope_defaults :default, action: '/doorman/unauthenticated'
|
39
|
+
|
40
|
+
config.failure_app = lambda { |_env|
|
41
|
+
notify :error, Doorman[:auth_required]
|
42
|
+
[302, { 'Location' => Doorman.configuration.paths[:login] }, ['']]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
Warden::Manager.before_failure do |env, _opts|
|
47
|
+
# Because authentication failure can happen on any request but
|
48
|
+
# we handle it only under "post '/doorman/unauthenticated'",
|
49
|
+
# we need o change request to POST
|
50
|
+
env['REQUEST_METHOD'] = 'POST'
|
51
|
+
# And we need to do the following to work with Rack::MethodOverride
|
52
|
+
env.each do |key, _value|
|
53
|
+
env[key]['_method'] = 'post' if key == 'rack.request.form_hash'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
Warden::Strategies.add(:password, Doorman::Strategies::Password)
|
57
|
+
|
58
|
+
app.post '/doorman/unauthenticated' do
|
59
|
+
status 401
|
60
|
+
session[:return_to] = env['warden.options'][:attempted_path] if session[:return_to].nil?
|
61
|
+
redirect(Doorman.configuration.paths[:login])
|
62
|
+
end
|
63
|
+
|
64
|
+
app.get '/doorman/register/?' do
|
65
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
66
|
+
|
67
|
+
unless Doorman.configuration.registration
|
68
|
+
notify :error, :signup_disabled
|
69
|
+
redirect(Doorman.configuration.paths[:login])
|
70
|
+
end
|
71
|
+
|
72
|
+
haml :register
|
73
|
+
end
|
74
|
+
|
75
|
+
app.post '/doorman/register' do
|
76
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
77
|
+
|
78
|
+
unless Doorman.configuration[:registration]
|
79
|
+
notify :error, :signup_disabled
|
80
|
+
redirect(Doorman.configuration.paths[:login])
|
81
|
+
end
|
82
|
+
|
83
|
+
begin
|
84
|
+
if User.registered?(params[:user][:username])
|
85
|
+
notify :error, :register_account_exists
|
86
|
+
redirect(Doorman.configuration.paths[:login])
|
87
|
+
end
|
88
|
+
|
89
|
+
user = User.make(
|
90
|
+
username: params[:user][:username],
|
91
|
+
password: params[:user][:password],
|
92
|
+
confirmed: !Doorman.configuration[:confirmation]
|
93
|
+
)
|
94
|
+
user.name = {
|
95
|
+
first_name: params[:user][:first_name],
|
96
|
+
last_name: params[:user][:last_name]
|
97
|
+
}
|
98
|
+
user.save
|
99
|
+
|
100
|
+
if Doorman.configuration[:confirmation]
|
101
|
+
token = user.confirm if Doorman.configuration[:confirmation]
|
102
|
+
|
103
|
+
if File.exist?('views/doorman/templates/account_activation.haml')
|
104
|
+
template = haml :'/templates/account_activation', layout: false, locals: {
|
105
|
+
user: user.username,
|
106
|
+
link: token_link('confirm', token.token)
|
107
|
+
}
|
108
|
+
Doorman::Mailer.new(
|
109
|
+
to: user.username,
|
110
|
+
subject: 'Account activation request',
|
111
|
+
text: "You have to activate your account (#{user.username}) before using this service. " + token_link('confirm', token.token),
|
112
|
+
html: template
|
113
|
+
).deliver!
|
114
|
+
else
|
115
|
+
Doorman.logger.warn "Template not found (views/doorman/templates/account_activation.haml), account activation at #{token_link('confirm', token.token)}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Login when registration is completed
|
120
|
+
warden.authenticate(:password)
|
121
|
+
|
122
|
+
# Set cookie
|
123
|
+
cookies[:email] = params[:user][:username]
|
124
|
+
|
125
|
+
notify :success, :signup_success
|
126
|
+
redirect(Doorman.configuration.paths[:success])
|
127
|
+
rescue => e
|
128
|
+
notify :error, e.message
|
129
|
+
redirect(Doorman.configuration.paths[:login])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
app.get '/doorman/confirm/:token/?' do
|
134
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
135
|
+
|
136
|
+
if params[:token].nil? || params[:token].empty?
|
137
|
+
notify :error, :token_not_found
|
138
|
+
redirect(back)
|
139
|
+
end
|
140
|
+
|
141
|
+
token = Token.where(token: params[:token]).first
|
142
|
+
if token.nil? && !token&.type.eql?(:confirm)
|
143
|
+
notify :error, :token_not_found
|
144
|
+
redirect(back)
|
145
|
+
end
|
146
|
+
if token&.used?
|
147
|
+
notify :error, :token_used
|
148
|
+
redirect(back)
|
149
|
+
end
|
150
|
+
|
151
|
+
user = token&.user
|
152
|
+
if user.nil?
|
153
|
+
notify :error, :confirm_no_user
|
154
|
+
redirect(Doorman.config.paths[:login])
|
155
|
+
end
|
156
|
+
|
157
|
+
user&.confirm!
|
158
|
+
notify :success, :confirm_success
|
159
|
+
redirect(Doorman.configuration.paths[:login])
|
160
|
+
end
|
161
|
+
|
162
|
+
app.get '/doorman/login/?' do
|
163
|
+
redirect(Doorman.configuration.paths[:success]) if authenticated?
|
164
|
+
|
165
|
+
email = cookies[:email]
|
166
|
+
email = params[:email] if email.nil?
|
167
|
+
|
168
|
+
haml :login, locals: { email: email }
|
169
|
+
end
|
170
|
+
|
171
|
+
app.post '/doorman/login' do
|
172
|
+
warden.authenticate(:password)
|
173
|
+
|
174
|
+
# Set cookie
|
175
|
+
cookies[:email] = params[:user][:username]
|
176
|
+
|
177
|
+
# Notify if there are any messages from Warden.
|
178
|
+
notify :error, warden.message unless warden.message.blank?
|
179
|
+
|
180
|
+
redirect(Doorman.configuration.use_referrer && session[:return_to] ? session.delete(:return_to) : Doorman.configuration.paths[:success])
|
181
|
+
end
|
182
|
+
|
183
|
+
app.get '/doorman/logout/?' do
|
184
|
+
warden.logout(:default)
|
185
|
+
|
186
|
+
notify :success, :logout_success
|
187
|
+
redirect(Doorman.configuration.paths[:login])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class Middleware < Sinatra::Base
|
193
|
+
helpers Sinatra::Cookies
|
194
|
+
register Sinatra::Flash
|
195
|
+
register Sinatra::Partial
|
196
|
+
register Strategies::ForgotPassword
|
197
|
+
register Strategies::RememberMe
|
198
|
+
register Doorman::Base
|
199
|
+
register Doorman::Providers::Bitbucket
|
200
|
+
register Doorman::Providers::GitHub
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
class Configuration
|
6
|
+
def self.config_option(name)
|
7
|
+
define_method(name) do
|
8
|
+
read_value(name)
|
9
|
+
end
|
10
|
+
|
11
|
+
define_method("#{name}=") do |value|
|
12
|
+
set_value(name, value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
#def self.proc_config_option(name)
|
17
|
+
# define_method(name) do |&block|
|
18
|
+
# set_value(name, block) unless block.nil?
|
19
|
+
# read_value(name)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# define_method("#{name}=") do |value|
|
23
|
+
# set_value(name, value)
|
24
|
+
# end
|
25
|
+
#end
|
26
|
+
|
27
|
+
# Set log level
|
28
|
+
config_option :log_level
|
29
|
+
|
30
|
+
# Enables/disables user confirmation
|
31
|
+
config_option :confirmation
|
32
|
+
# Enables/disables user registration
|
33
|
+
config_option :registration
|
34
|
+
# Enables/disables the usage of referrer redirection
|
35
|
+
config_option :use_referrer
|
36
|
+
|
37
|
+
# Remember me cookie name
|
38
|
+
config_option :remember_cookie
|
39
|
+
# Remember me for x-days
|
40
|
+
config_option :remember_for
|
41
|
+
|
42
|
+
# Database name
|
43
|
+
config_option :db_name
|
44
|
+
# Database client
|
45
|
+
config_option :db_client
|
46
|
+
|
47
|
+
# SMTP Domain
|
48
|
+
config_option :smtp_domain
|
49
|
+
# SMTP Server
|
50
|
+
config_option :smtp_server
|
51
|
+
# SMTP Server PSort
|
52
|
+
config_option :smtp_port
|
53
|
+
# SMTP Sender Username
|
54
|
+
config_option :smtp_username
|
55
|
+
# SMTP Sender Password
|
56
|
+
config_option :smtp_password
|
57
|
+
|
58
|
+
# Authentication Providers
|
59
|
+
config_option :providers
|
60
|
+
|
61
|
+
# Authentication Providers
|
62
|
+
config_option :paths
|
63
|
+
|
64
|
+
attr_reader :defaults
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
@config_values = {}
|
68
|
+
|
69
|
+
# set default attribute values
|
70
|
+
@defaults = _defaults
|
71
|
+
end
|
72
|
+
|
73
|
+
def [](key)
|
74
|
+
read_value(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
def []=(key, value)
|
78
|
+
set_value(key, value)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def read_value(name)
|
84
|
+
if @config_values.key?(name)
|
85
|
+
@config_values[name]
|
86
|
+
else
|
87
|
+
@defaults.send(name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_value(name, value)
|
92
|
+
@config_values[name] = value
|
93
|
+
end
|
94
|
+
|
95
|
+
def _defaults
|
96
|
+
OpenStruct.new(
|
97
|
+
log_level: Logger::DEBUG,
|
98
|
+
confirmation: false,
|
99
|
+
registration: false,
|
100
|
+
use_referrer: true,
|
101
|
+
remember_cookie: 'sinatra.doorman.remember',
|
102
|
+
remember_for: 30,
|
103
|
+
db_name: 'doorman',
|
104
|
+
db_client: :doorman,
|
105
|
+
smtp_domain: 'doorman.local',
|
106
|
+
smtp_server: '127.0.0.1',
|
107
|
+
smtp_port: 587,
|
108
|
+
smtp_username: nil,
|
109
|
+
smtp_password: nil,
|
110
|
+
providers: [],
|
111
|
+
paths: {
|
112
|
+
success: '/home',
|
113
|
+
login: '/doorman/login',
|
114
|
+
logout: '/doorman/logout',
|
115
|
+
forgot: '/doorman/forgot',
|
116
|
+
reset: '/doorman/reset',
|
117
|
+
register: '/doorman/register'
|
118
|
+
}
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :code
|
7
|
+
attr_reader :field
|
8
|
+
attr_reader :error
|
9
|
+
|
10
|
+
ERRORS = {
|
11
|
+
invalid_api_method: 'No method parameter was supplied',
|
12
|
+
unspecified_error: 'Unspecified error',
|
13
|
+
already_exists: '{what}',
|
14
|
+
does_not_exist: '{what}',
|
15
|
+
does_not_match: '{what}',
|
16
|
+
account: '{what}',
|
17
|
+
invalid_date: 'Cannot parse {what} from: {date}',
|
18
|
+
invalid_type: '{what}',
|
19
|
+
not_active: 'Not active',
|
20
|
+
required_field_missing: 'Required field {field} is missing'
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
def initialize(code, params = {})
|
24
|
+
field = params.delete(:field)
|
25
|
+
error = params.delete(:error)
|
26
|
+
|
27
|
+
super(parse(code, params))
|
28
|
+
@code = code || :unspecified_error
|
29
|
+
@field = field || :unspecified_field
|
30
|
+
@error = error
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse(code, params = {})
|
36
|
+
message = ERRORS[code]
|
37
|
+
params.each_pair do |key, value|
|
38
|
+
message = message.sub("{#{key}}", value)
|
39
|
+
end
|
40
|
+
message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Obscured
|
4
|
+
module Doorman
|
5
|
+
module Helpers
|
6
|
+
# The main accessor to the warden middleware
|
7
|
+
def warden
|
8
|
+
request.env['warden']
|
9
|
+
end
|
10
|
+
|
11
|
+
# Check the current session is authenticated to a given scope
|
12
|
+
def authenticated?(scope = nil)
|
13
|
+
scope ? warden.authenticated?(scope: scope) : warden.authenticated?
|
14
|
+
end
|
15
|
+
alias logged_in? authenticated?
|
16
|
+
|
17
|
+
# Authenticate a user against defined strategies
|
18
|
+
def authenticate(*args)
|
19
|
+
warden.authenticate!(*args)
|
20
|
+
end
|
21
|
+
alias login authenticate
|
22
|
+
|
23
|
+
# Return session info
|
24
|
+
#
|
25
|
+
# @param [Symbol] scope the scope to retrieve session info for
|
26
|
+
def session_info(scope = nil)
|
27
|
+
scope ? warden.session(scope) : scope
|
28
|
+
end
|
29
|
+
|
30
|
+
# Terminate the current session
|
31
|
+
#
|
32
|
+
# @param [Symbol] scopes the session scope to terminate
|
33
|
+
def logout(scopes = nil)
|
34
|
+
scopes ? warden.logout(scopes) : warden.logout(warden.config.default_scope)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Access the user from the current session
|
38
|
+
#
|
39
|
+
# @param [Symbol] scope for the logged in user
|
40
|
+
def user(scope = nil)
|
41
|
+
scope ? warden.user(scope) : warden.user
|
42
|
+
end
|
43
|
+
alias current_user user
|
44
|
+
|
45
|
+
# Require authorization for an action
|
46
|
+
#
|
47
|
+
# @param [String] failure_path path to redirect to if user is unauthenticated
|
48
|
+
def authorize!(failure_path = nil)
|
49
|
+
unless authenticated?
|
50
|
+
session[:return_to] = request.path if Doorman.configuration.use_referrer
|
51
|
+
redirect(failure_path || Doorman.configuration.paths[:login])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Require authorization for example ajax calls, returns 403 is not authenticated
|
56
|
+
#
|
57
|
+
# @param [Symbol] format
|
58
|
+
def authorized?(format = :json)
|
59
|
+
unless authenticated?
|
60
|
+
halt 403, { 'Content-Type' => 'application/json' }, { message: 'Unauthorized' }.to_json if format == :json
|
61
|
+
halt 403
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|