obscured-doorman 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|