masq2 1.0.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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +83 -0
- data/CODE_OF_CONDUCT.md +135 -0
- data/CONTRIBUTING.md +151 -0
- data/LICENSE.txt +22 -0
- data/README.md +426 -0
- data/SECURITY.md +23 -0
- data/app/assets/images/masq/favicon.ico +0 -0
- data/app/assets/images/masq/openid_symbol.png +0 -0
- data/app/assets/images/masq/seatbelt_icon.png +0 -0
- data/app/assets/images/masq/seatbelt_icon_gray.png +0 -0
- data/app/assets/images/masq/seatbelt_icon_high.png +0 -0
- data/app/assets/stylesheets/masq/application.css.erb +61 -0
- data/app/controllers/masq/accounts_controller.rb +132 -0
- data/app/controllers/masq/base_controller.rb +78 -0
- data/app/controllers/masq/consumer_controller.rb +144 -0
- data/app/controllers/masq/info_controller.rb +23 -0
- data/app/controllers/masq/passwords_controller.rb +42 -0
- data/app/controllers/masq/personas_controller.rb +75 -0
- data/app/controllers/masq/server_controller.rb +247 -0
- data/app/controllers/masq/sessions_controller.rb +58 -0
- data/app/controllers/masq/sites_controller.rb +60 -0
- data/app/controllers/masq/yubikey_associations_controller.rb +25 -0
- data/app/helpers/masq/application_helper.rb +57 -0
- data/app/helpers/masq/personas_helper.rb +15 -0
- data/app/helpers/masq/server_helper.rb +15 -0
- data/app/mailers/masq/account_mailer.rb +17 -0
- data/app/models/masq/account.rb +245 -0
- data/app/models/masq/open_id_request.rb +42 -0
- data/app/models/masq/persona.rb +61 -0
- data/app/models/masq/release_policy.rb +11 -0
- data/app/models/masq/site.rb +68 -0
- data/app/views/layouts/masq/base.html.erb +70 -0
- data/app/views/layouts/masq/consumer.html.erb +30 -0
- data/app/views/masq/account_mailer/forgot_password.html.erb +3 -0
- data/app/views/masq/account_mailer/forgot_password.text.erb +3 -0
- data/app/views/masq/account_mailer/signup_notification.html.erb +5 -0
- data/app/views/masq/account_mailer/signup_notification.text.erb +5 -0
- data/app/views/masq/accounts/_hcard.html.erb +29 -0
- data/app/views/masq/accounts/edit.html.erb +100 -0
- data/app/views/masq/accounts/new.html.erb +27 -0
- data/app/views/masq/accounts/show.html.erb +4 -0
- data/app/views/masq/accounts/show.xrds.builder +40 -0
- data/app/views/masq/consumer/index.html.erb +31 -0
- data/app/views/masq/consumer/start.html.erb +2 -0
- data/app/views/masq/info/help.html.erb +8 -0
- data/app/views/masq/info/index.html.erb +5 -0
- data/app/views/masq/info/safe_login.html.erb +24 -0
- data/app/views/masq/passwords/edit.html.erb +13 -0
- data/app/views/masq/passwords/new.html.erb +11 -0
- data/app/views/masq/personas/_form.html.erb +159 -0
- data/app/views/masq/personas/edit.html.erb +9 -0
- data/app/views/masq/personas/index.html.erb +17 -0
- data/app/views/masq/personas/new.html.erb +9 -0
- data/app/views/masq/server/decide.html.erb +146 -0
- data/app/views/masq/server/index.xrds.builder +19 -0
- data/app/views/masq/server/seatbelt_config.xml.builder +24 -0
- data/app/views/masq/server/seatbelt_login_state.xml.builder +4 -0
- data/app/views/masq/sessions/new.html.erb +27 -0
- data/app/views/masq/shared/_error_messages.html.erb +12 -0
- data/app/views/masq/sites/edit.html.erb +42 -0
- data/app/views/masq/sites/index.html.erb +20 -0
- data/config/initializers/configuration.rb +5 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/initializers/requires.rb +6 -0
- data/config/locales/de.yml +281 -0
- data/config/locales/en.yml +271 -0
- data/config/locales/es.yml +254 -0
- data/config/locales/nl.yml +258 -0
- data/config/locales/rails.de.yml +225 -0
- data/config/locales/ru.yml +272 -0
- data/config/masq.example.yml +132 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20120312120000_masq_schema.rb +152 -0
- data/db/migrate/20130626220915_remame_last_authenticated_with_yubikey_on_masq_accounts.rb +11 -0
- data/db/migrate/20130704205532_add_first_and_lastname_columns_to_personas.rb +11 -0
- data/db/migrate/20130807060329_change_open_id_associations_server_url_column_type.rb +22 -0
- data/lib/masq/active_record_openid_store/association.rb +16 -0
- data/lib/masq/active_record_openid_store/nonce.rb +9 -0
- data/lib/masq/active_record_openid_store/openid_ar_store.rb +58 -0
- data/lib/masq/authenticated_system.rb +136 -0
- data/lib/masq/engine.rb +12 -0
- data/lib/masq/openid_server_system.rb +110 -0
- data/lib/masq/signup.rb +53 -0
- data/lib/masq/version.rb +5 -0
- data/lib/masq.rb +50 -0
- data/lib/masq2.rb +1 -0
- data/lib/tasks/masq_tasks.rake +58 -0
- data.tar.gz.sig +2 -0
- metadata +377 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module Masq
|
2
|
+
class SitesController < BaseController
|
3
|
+
before_action :login_required
|
4
|
+
before_action :find_personas, only: [:create, :edit, :update]
|
5
|
+
|
6
|
+
helper_method :site, :persona
|
7
|
+
|
8
|
+
def index
|
9
|
+
@sites = current_account.sites.includes(:persona).order(:url)
|
10
|
+
|
11
|
+
respond_to do |format|
|
12
|
+
format.html
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def edit
|
17
|
+
site.persona = current_account.personas.find(params[:persona_id]) if params[:persona_id]
|
18
|
+
end
|
19
|
+
|
20
|
+
def update
|
21
|
+
respond_to do |format|
|
22
|
+
if site.update(site_params)
|
23
|
+
flash[:notice] = t(:release_policy_for_site_updated)
|
24
|
+
format.html { redirect_to(edit_account_site_path(site)) }
|
25
|
+
else
|
26
|
+
format.html { render(action: "edit") }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
site.destroy
|
33
|
+
|
34
|
+
respond_to do |format|
|
35
|
+
format.html { redirect_to(account_sites_path) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def site
|
45
|
+
@site ||= current_account.sites.find(params[:id])
|
46
|
+
end
|
47
|
+
|
48
|
+
def persona
|
49
|
+
@persona ||= site.persona
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_personas
|
53
|
+
@personas = current_account.personas.order(:title)
|
54
|
+
end
|
55
|
+
|
56
|
+
def site_params
|
57
|
+
params.require(:site).permit(:persona_id, :url, properties: {})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Masq
|
2
|
+
class YubikeyAssociationsController < BaseController
|
3
|
+
before_action :login_required
|
4
|
+
|
5
|
+
def create
|
6
|
+
if current_account.associate_with_yubikey(params[:yubico_otp])
|
7
|
+
flash[:notice] = t(:account_associated_with_yubico_identity)
|
8
|
+
else
|
9
|
+
flash[:alert] = t(:sorry_yubico_one_time_password_incorrect)
|
10
|
+
end
|
11
|
+
respond_to do |format|
|
12
|
+
format.html { redirect_to(edit_account_path) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
current_account.yubico_identity = nil
|
18
|
+
current_account.save
|
19
|
+
|
20
|
+
respond_to do |format|
|
21
|
+
format.html { redirect_to(edit_account_path, notice: t(:account_disassociated_from_yubico_identity)) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Masq
|
2
|
+
module ApplicationHelper
|
3
|
+
def page_title
|
4
|
+
(@page_title ||= nil) ? "#{@page_title} | #{Masq::Engine.config.masq["name"]}" : Masq::Engine.config.masq["name"]
|
5
|
+
end
|
6
|
+
|
7
|
+
def label_tag(field, text = nil, options = {})
|
8
|
+
content_tag(:label, text ? text : field.to_s.humanize, options.reverse_merge(for: field.to_s))
|
9
|
+
end
|
10
|
+
|
11
|
+
def error_messages_for(*objects)
|
12
|
+
render("masq/shared/error_messages", objects: objects.flatten)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Is the current page an identity page? This is used to display
|
16
|
+
# further information (like the endoint url) in the <head>
|
17
|
+
def identity_page?
|
18
|
+
active_page?("accounts" => ["show"])
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is the current page the home page? This is used to display
|
22
|
+
# further information (like the endoint url) in the <head>
|
23
|
+
def home_page?
|
24
|
+
active_page?("info" => ["index"])
|
25
|
+
end
|
26
|
+
|
27
|
+
# Custom label names for request properties (like SReg data)
|
28
|
+
def property_label_text(property)
|
29
|
+
case property.to_sym
|
30
|
+
when :image_default then t(:image_url)
|
31
|
+
when :web_default then t(:website_url)
|
32
|
+
when :web_blog then t(:blog_url)
|
33
|
+
else t(property.to_sym)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def property_label_text_for_type_uri(type_uri)
|
38
|
+
property = Persona.attribute_name_for_type_uri(type_uri)
|
39
|
+
property ? property_label_text(property) : type_uri
|
40
|
+
end
|
41
|
+
|
42
|
+
# Renders a navigation element and marks it as active where
|
43
|
+
# appropriate. See active_page? for details
|
44
|
+
def nav(name, url, pages = nil, active = false)
|
45
|
+
content_tag(:li, link_to(name, url), class: ((active || (pages && active_page?(pages))) ? "act" : nil))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Takes a hash with pages and tells whether the current page is among them.
|
49
|
+
# The keys must be controller names and their value must be an array of
|
50
|
+
# action names. If the array is empty, every action is supposed to be valid.
|
51
|
+
def active_page?(pages = {})
|
52
|
+
is_active = pages.include?(params[:controller])
|
53
|
+
is_active = pages[params[:controller]].include?(params[:action]) if is_active && !pages[params[:controller]].empty?
|
54
|
+
is_active
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "i18n_data"
|
2
|
+
|
3
|
+
module Masq
|
4
|
+
module PersonasHelper
|
5
|
+
# get list of codes and names sorted by country name
|
6
|
+
def countries_for_select
|
7
|
+
::I18nData.countries.map { |pair| pair.reverse }.sort_by(&:first)
|
8
|
+
end
|
9
|
+
|
10
|
+
# get list of codes and names sorted by language name
|
11
|
+
def languages_for_select
|
12
|
+
::I18nData.languages.map { |pair| pair.reverse }.sort_by(&:first)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Masq
|
2
|
+
module ServerHelper
|
3
|
+
def sreg_request_for_field(field_name)
|
4
|
+
if sreg_request.required.include?(field_name)
|
5
|
+
t(:required)
|
6
|
+
elsif sreg_request.optional.include?(field_name)
|
7
|
+
t(:optional)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def ax_request_for_field(ax_attribute)
|
12
|
+
ax_attribute.required ? t(:required) : t(:optional)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Masq
|
2
|
+
class AccountMailer < ActionMailer::Base
|
3
|
+
default from: Masq::Engine.config.masq["email"]
|
4
|
+
default_url_options[:host] = Masq::Engine.config.masq["host"]
|
5
|
+
|
6
|
+
def signup_notification(account)
|
7
|
+
raise "send_activation_mail deactivated" unless Masq::Engine.config.masq["send_activation_mail"]
|
8
|
+
@account = account
|
9
|
+
mail(to: account.email, subject: I18n.t(:please_activate_your_account))
|
10
|
+
end
|
11
|
+
|
12
|
+
def forgot_password(account)
|
13
|
+
@account = account
|
14
|
+
mail(to: account.email, subject: I18n.t(:your_request_for_a_new_password))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
|
3
|
+
module Masq
|
4
|
+
class Account < ActiveRecord::Base
|
5
|
+
has_many :personas, ->() { order(:id) }, dependent: :delete_all
|
6
|
+
has_many :sites, dependent: :destroy
|
7
|
+
belongs_to :public_persona, class_name: "Persona", optional: true
|
8
|
+
|
9
|
+
validates_presence_of :login
|
10
|
+
validates_length_of :login, within: 3..254
|
11
|
+
validates_uniqueness_of :login, case_sensitive: false
|
12
|
+
validates_format_of :login, with: /\A[A-Za-z0-9_@.-]+\z/
|
13
|
+
validates_presence_of :email
|
14
|
+
validates_uniqueness_of :email, case_sensitive: false
|
15
|
+
validates_format_of :email, with: /(\A([^@\s]+)@((?:[-_a-z0-9]+\.)+[a-z]{2,})\z)/i, allow_blank: true
|
16
|
+
validates_presence_of :password, if: :password_required?
|
17
|
+
validates_presence_of :password_confirmation, if: :password_required?
|
18
|
+
validates_length_of :password, within: 6..40, if: :password_required?
|
19
|
+
validates_confirmation_of :password, if: :password_required?
|
20
|
+
# check `rake routes' for whether this list is still complete when routes are changed
|
21
|
+
validates_exclusion_of :login, in: %w[account session password help safe-login forgot_password reset_password login logout server consumer]
|
22
|
+
|
23
|
+
before_save :encrypt_password
|
24
|
+
after_save :deliver_forgot_password
|
25
|
+
|
26
|
+
# attr_accessible :login, :email, :password, :password_confirmation, :public_persona_id, :yubikey_mandatory
|
27
|
+
attr_accessor :password
|
28
|
+
|
29
|
+
class ActivationCodeNotFound < StandardError; end
|
30
|
+
|
31
|
+
class AlreadyActivated < StandardError
|
32
|
+
attr_reader :user, :message
|
33
|
+
def initialize(account, message = nil)
|
34
|
+
@message, @account = message, account
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# Finds the user with the corresponding activation code, activates their account and returns the user.
|
40
|
+
#
|
41
|
+
# Raises:
|
42
|
+
# [Account::ActivationCodeNotFound] if there is no user with the corresponding activation code
|
43
|
+
# [Account::AlreadyActivated] if the user with the corresponding activation code has already activated their account
|
44
|
+
def find_and_activate!(activation_code)
|
45
|
+
raise ArgumentError if activation_code.nil?
|
46
|
+
user = find_by(activation_code: activation_code)
|
47
|
+
raise ActivationCodeNotFound unless user
|
48
|
+
raise AlreadyActivated.new(user) if user.active?
|
49
|
+
user.send(:activate!)
|
50
|
+
user
|
51
|
+
end
|
52
|
+
|
53
|
+
# Authenticates a user by their login name and password.
|
54
|
+
# Returns the user or nil.
|
55
|
+
def authenticate(login, password, basic_auth_used = false)
|
56
|
+
a = find_by(login: login)
|
57
|
+
if a.nil? && Masq::Engine.config.masq["create_auth_ondemand"]["enabled"]
|
58
|
+
# Need to set some password - but is never used
|
59
|
+
pw = if Masq::Engine.config.masq["create_auth_ondemand"]["random_password"]
|
60
|
+
SecureRandom.hex(13)
|
61
|
+
else
|
62
|
+
password
|
63
|
+
end
|
64
|
+
signup = Masq::Signup.create_account!(
|
65
|
+
login: login,
|
66
|
+
password: pw,
|
67
|
+
password_confirmation: pw,
|
68
|
+
email: "#{login}@#{Masq::Engine.config.masq["create_auth_ondemand"]["default_mail_domain"]}",
|
69
|
+
)
|
70
|
+
a = signup.account if signup.succeeded?
|
71
|
+
end
|
72
|
+
|
73
|
+
if !a.nil? && a.active? && a.enabled
|
74
|
+
if a.authenticated?(password) || (Masq::Engine.config.masq["trust_basic_auth"] && basic_auth_used)
|
75
|
+
a.last_authenticated_at = Time.now.utc
|
76
|
+
a.last_authenticated_by_yubikey = a.authenticated_with_yubikey?
|
77
|
+
a.save(validate: false)
|
78
|
+
a
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Encrypts some data with the salt.
|
84
|
+
def encrypt(password, salt)
|
85
|
+
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Receives a login token which consists of the users password and
|
89
|
+
# a Yubico one time password (the otp is always 44 characters long)
|
90
|
+
def split_password_and_yubico_otp(token)
|
91
|
+
token.reverse!
|
92
|
+
yubico_otp = token.slice!(0..43).reverse
|
93
|
+
password = token.reverse
|
94
|
+
[password, yubico_otp]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the first twelve chars from the Yubico OTP,
|
98
|
+
# which are used to identify a Yubikey
|
99
|
+
def extract_yubico_identity_from_otp(yubico_otp)
|
100
|
+
yubico_otp[0..11]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Utilizes the Yubico library to verify a one time password
|
104
|
+
def verify_yubico_otp(otp)
|
105
|
+
Yubikey::OTP::Verify.new(otp).valid?
|
106
|
+
rescue Yubikey::OTP::InvalidOTPError
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_param
|
112
|
+
login
|
113
|
+
end
|
114
|
+
|
115
|
+
# The existence of an activation code means they have not activated yet
|
116
|
+
def active?
|
117
|
+
activation_code.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def activate!
|
121
|
+
@activated = true
|
122
|
+
self.activated_at = Time.now.utc
|
123
|
+
self.activation_code = nil
|
124
|
+
save
|
125
|
+
end
|
126
|
+
|
127
|
+
# True if the user has just been activated
|
128
|
+
def pending?
|
129
|
+
@activated ||= false
|
130
|
+
end
|
131
|
+
|
132
|
+
# Does the user have the possibility to authenticate with a one time password?
|
133
|
+
def has_otp_device?
|
134
|
+
!yubico_identity.nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
# Encrypts the password with the user salt
|
138
|
+
def encrypt(password)
|
139
|
+
self.class.encrypt(password, salt)
|
140
|
+
end
|
141
|
+
|
142
|
+
def authenticated?(password)
|
143
|
+
if password.nil?
|
144
|
+
false
|
145
|
+
elsif password.length < 50 && !(yubico_identity? && yubikey_mandatory?)
|
146
|
+
encrypt(password) == crypted_password
|
147
|
+
elsif Masq::Engine.config.masq["can_use_yubikey"]
|
148
|
+
password, yubico_otp = self.class.split_password_and_yubico_otp(password)
|
149
|
+
@authenticated_with_yubikey = yubikey_authenticated?(yubico_otp) if encrypt(password) == crypted_password
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Is the Yubico OTP valid and belongs to this account?
|
154
|
+
def yubikey_authenticated?(otp)
|
155
|
+
if yubico_identity? && self.class.verify_yubico_otp(otp)
|
156
|
+
(self.class.extract_yubico_identity_from_otp(otp) == yubico_identity)
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def authenticated_with_yubikey?
|
163
|
+
@authenticated_with_yubikey ||= false
|
164
|
+
end
|
165
|
+
|
166
|
+
def associate_with_yubikey(otp)
|
167
|
+
if self.class.verify_yubico_otp(otp)
|
168
|
+
self.yubico_identity = self.class.extract_yubico_identity_from_otp(otp)
|
169
|
+
save(validate: false)
|
170
|
+
else
|
171
|
+
false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def remember_token?
|
176
|
+
remember_token_expires_at && Time.now.utc < remember_token_expires_at
|
177
|
+
end
|
178
|
+
|
179
|
+
# These create and unset the fields required for remembering users between browser closes
|
180
|
+
def remember_me
|
181
|
+
remember_me_for(2.weeks)
|
182
|
+
end
|
183
|
+
|
184
|
+
def remember_me_for(time)
|
185
|
+
remember_me_until(time.from_now.utc)
|
186
|
+
end
|
187
|
+
|
188
|
+
def remember_me_until(time)
|
189
|
+
self.remember_token_expires_at = time
|
190
|
+
self.remember_token = encrypt("#{email}--#{remember_token_expires_at}")
|
191
|
+
save(validate: false)
|
192
|
+
end
|
193
|
+
|
194
|
+
def forget_me
|
195
|
+
self.remember_token_expires_at = nil
|
196
|
+
self.remember_token = nil
|
197
|
+
save(validate: false)
|
198
|
+
end
|
199
|
+
|
200
|
+
def forgot_password!
|
201
|
+
@forgotten_password = true
|
202
|
+
make_password_reset_code
|
203
|
+
save
|
204
|
+
end
|
205
|
+
|
206
|
+
# First update the password_reset_code before setting the
|
207
|
+
# reset_password flag to avoid duplicate email notifications.
|
208
|
+
def reset_password
|
209
|
+
update_attribute(:password_reset_code, nil)
|
210
|
+
@reset_password = true
|
211
|
+
end
|
212
|
+
|
213
|
+
def recently_forgot_password?
|
214
|
+
@forgotten_password ||= false
|
215
|
+
end
|
216
|
+
|
217
|
+
def recently_reset_password?
|
218
|
+
@reset_password ||= false
|
219
|
+
end
|
220
|
+
|
221
|
+
def disable!
|
222
|
+
update_attribute(:enabled, false)
|
223
|
+
end
|
224
|
+
|
225
|
+
protected
|
226
|
+
|
227
|
+
def encrypt_password
|
228
|
+
return if password.blank?
|
229
|
+
self.salt = Digest::SHA1.hexdigest("--#{Time.now}--#{login}--") if new_record?
|
230
|
+
self.crypted_password = encrypt(password)
|
231
|
+
end
|
232
|
+
|
233
|
+
def password_required?
|
234
|
+
crypted_password.blank? || !password.blank?
|
235
|
+
end
|
236
|
+
|
237
|
+
def make_password_reset_code
|
238
|
+
self.password_reset_code = Digest::SHA1.hexdigest(Time.now.to_s.split("").sort_by { rand }.join)
|
239
|
+
end
|
240
|
+
|
241
|
+
def deliver_forgot_password
|
242
|
+
Masq::AccountMailer.forgot_password(self).deliver_now if recently_forgot_password?
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Masq
|
2
|
+
class OpenIdRequest < ActiveRecord::Base
|
3
|
+
validates_presence_of :token, :parameters
|
4
|
+
|
5
|
+
before_validation :make_token, on: :create
|
6
|
+
|
7
|
+
if Rails.gem_version >= Gem::Version.create("6.1")
|
8
|
+
serialize :parameters, type: Hash, coder: JSON
|
9
|
+
else # Rails 5.2 & 6.0
|
10
|
+
serialize :parameters, JSON
|
11
|
+
end
|
12
|
+
|
13
|
+
def parameters
|
14
|
+
self[:parameters]
|
15
|
+
end
|
16
|
+
|
17
|
+
def parameters=(params)
|
18
|
+
self[:parameters] =
|
19
|
+
case params
|
20
|
+
# arbitrary params passed as Hash
|
21
|
+
when Hash
|
22
|
+
params.delete_if { |k, _v| k.index("openid.") != 0 }
|
23
|
+
# params from ActionController (does not inherit directly from HashWithIndifferentAccess after Rails 4.2)
|
24
|
+
when ActionController::Parameters
|
25
|
+
params.to_unsafe_h.delete_if { |k, _v| k.index("openid.") != 0 }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def from_trusted_domain?
|
30
|
+
host = URI.parse(parameters["openid.realm"] || parameters["openid.trust_root"]).host
|
31
|
+
return false if Masq::Engine.config.masq["trusted_domains"].nil?
|
32
|
+
|
33
|
+
Masq::Engine.config.masq["trusted_domains"].find { |domain| host.to_s.ends_with?(domain) }
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def make_token
|
39
|
+
self.token = Digest::SHA1.hexdigest(Time.now.to_s.split("").sort_by { rand }.join)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Masq
|
2
|
+
class Persona < ActiveRecord::Base
|
3
|
+
belongs_to :account
|
4
|
+
has_many :sites, dependent: :destroy
|
5
|
+
|
6
|
+
validates_presence_of :account
|
7
|
+
validates_presence_of :title
|
8
|
+
validates_uniqueness_of :title, scope: :account_id
|
9
|
+
|
10
|
+
before_destroy :check_deletable!
|
11
|
+
|
12
|
+
# attr_protected :account_id, :deletable
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def properties
|
16
|
+
mappings.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def attribute_name_for_type_uri(type_uri)
|
20
|
+
prop = mappings.detect { |i| i[1].include?(type_uri) }
|
21
|
+
prop ? prop[0] : nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Mappings for SReg names and AX Type URIs to attributes
|
25
|
+
def mappings
|
26
|
+
Masq::Engine.config.masq["attribute_mappings"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
public
|
31
|
+
|
32
|
+
# Returns the personas attribute for the given SReg name or AX Type URI
|
33
|
+
def property(type)
|
34
|
+
prop = Persona.mappings.detect { |i| i[1].include?(type) }
|
35
|
+
prop ? send(prop[0]).to_s : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def date_of_birth
|
39
|
+
"#{dob_year? ? dob_year : "0000"}-#{dob_month? ? dob_month.to_s.rjust(2, "0") : "00"}-#{dob_day? ? dob_day.to_s.rjust(2, "0") : "00"}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def fullname=(name)
|
43
|
+
self.firstname, self.surname = name.to_s.split(" ")
|
44
|
+
self.surname ||= firstname
|
45
|
+
self[:fullname] = name
|
46
|
+
end
|
47
|
+
|
48
|
+
def date_of_birth=(dob)
|
49
|
+
res = dob.split("-")
|
50
|
+
self.dob_year = res[0]
|
51
|
+
self.dob_month = res[1]
|
52
|
+
self.dob_day = res[2]
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def check_deletable!
|
58
|
+
raise ActiveRecord::RecordNotDestroyed unless deletable
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Masq
|
2
|
+
class ReleasePolicy < ActiveRecord::Base
|
3
|
+
belongs_to :site
|
4
|
+
|
5
|
+
validates_presence_of :site
|
6
|
+
validates_presence_of :property
|
7
|
+
validates_uniqueness_of :property, scope: [:site_id, :type_identifier]
|
8
|
+
|
9
|
+
# attr_accessible :property, :type_identifier
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Masq
|
2
|
+
class Site < ActiveRecord::Base
|
3
|
+
belongs_to :account
|
4
|
+
belongs_to :persona
|
5
|
+
has_many :release_policies, dependent: :destroy
|
6
|
+
|
7
|
+
validates_presence_of :url, :persona, :account
|
8
|
+
validates_uniqueness_of :url, scope: :account_id
|
9
|
+
# attr_accessible :url, :persona_id, :properties, :ax_fetch, :sreg
|
10
|
+
|
11
|
+
# Sets the release policies by first deleting the old ones and
|
12
|
+
# then appending a new one for every given sreg and ax property.
|
13
|
+
# This setter is used to set the attributes received from the
|
14
|
+
# update site form, so it gets passed AX and SReg properties.
|
15
|
+
# To be backwards compatible (SReg seems to be obsolete now that
|
16
|
+
# there is AX), SReg properties get a type_identifier matching
|
17
|
+
# their property name so that they can be distinguished from AX
|
18
|
+
# properties (see the sreg_properties and ax_properties getters).
|
19
|
+
def properties=(props)
|
20
|
+
release_policies.destroy_all
|
21
|
+
props.each_pair do |property, details|
|
22
|
+
release_policies.build(property: property, type_identifier: details["type"]) if details["value"]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generates a release policy for each property that has a value.
|
27
|
+
# This setter is used in the server controllers complete action
|
28
|
+
# to set the attributes recieved from the decision form.
|
29
|
+
def ax_fetch=(props)
|
30
|
+
props.each_pair do |property, details|
|
31
|
+
release_policies.build(property: property, type_identifier: details["type"]) if details["value"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates a release policy for each SReg property.
|
36
|
+
# This setter is used in the server controllers complete action
|
37
|
+
# to set the attributes recieved from the decision form.
|
38
|
+
def sreg=(props)
|
39
|
+
props.each_key do |property|
|
40
|
+
release_policies.build(property: property, type_identifier: property)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a hash with all released SReg properties. SReg properties
|
45
|
+
# have a type_identifier matching their property name
|
46
|
+
def sreg_properties
|
47
|
+
props = {}
|
48
|
+
release_policies.each do |rp|
|
49
|
+
is_sreg = (rp.property == rp.type_identifier)
|
50
|
+
props[rp.property] = persona.property(rp.property) if is_sreg
|
51
|
+
end
|
52
|
+
props
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a hash with all released AX properties.
|
56
|
+
# AX properties have a URL as type_identifier.
|
57
|
+
def ax_properties
|
58
|
+
props = {}
|
59
|
+
release_policies.each do |rp|
|
60
|
+
if rp.type_identifier.match?("://")
|
61
|
+
props["type.#{rp.property}"] = rp.type_identifier
|
62
|
+
props["value.#{rp.property}"] = persona.property(rp.type_identifier)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
props
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%=h page_title %></title>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
6
|
+
<% if identity_page? %>
|
7
|
+
<meta http-equiv="X-XRDS-Location" content="<%= identity_url(:account => @account, :format => :xrds, :protocol => scheme) %>" />
|
8
|
+
<link rel="openid.server openid2.provider" href="<%= endpoint_url %>" />
|
9
|
+
<% elsif home_page? %>
|
10
|
+
<meta http-equiv="X-XRDS-Location" content="<%= server_url(:format => :xrds, :protocol => scheme) %>" />
|
11
|
+
<% end %>
|
12
|
+
<link rel="seatbelt.config" type="application/xml" href="<%= seatbelt_config_url(:format => :xml, :protocol => scheme) %>" />
|
13
|
+
<link rel="Shortcut Icon" href="/favicon.ico" type="image/x-icon" />
|
14
|
+
<link rel="icon" href="/favicon.ico" type="image/ico" />
|
15
|
+
<%= stylesheet_link_tag 'masq/application' %>
|
16
|
+
<%= csrf_meta_tags %>
|
17
|
+
</head>
|
18
|
+
<body>
|
19
|
+
<div id="head">
|
20
|
+
<div class="wrap">
|
21
|
+
<h1><%= link_to Masq::Engine.config.masq['name'], root_path %></h1>
|
22
|
+
<ul id="navi">
|
23
|
+
<% if logged_in? %>
|
24
|
+
<% unless checkid_request %>
|
25
|
+
<%= nav t(:nav_identity), identity_path(current_account), 'accounts' => ['show'] %>
|
26
|
+
<%= nav t(:nav_profile), edit_account_path, 'accounts' => ['edit', 'update'] %>
|
27
|
+
<%= nav t(:nav_personas), account_personas_path, 'personas' => [] %>
|
28
|
+
<%= nav t(:nav_trusted_sites), account_sites_path, 'sites' => [] %>
|
29
|
+
<% if not auth_type_used == :basic %>
|
30
|
+
<%= nav t(:logout), logout_path %>
|
31
|
+
<% end %>
|
32
|
+
<% else %>
|
33
|
+
<%= nav t(:current_verification_request), proceed_path, 'server' => [] %>
|
34
|
+
<% end %>
|
35
|
+
<% else %>
|
36
|
+
<%= nav t(:login_link), login_path, 'sessions' => ['new', 'create'] %>
|
37
|
+
<% unless Masq::Engine.config.masq['disable_registration'] %>
|
38
|
+
<%= nav t(:signup_link), new_account_path, 'accounts' => ['new', 'create'] %>
|
39
|
+
<% end %>
|
40
|
+
<%= nav t(:help), help_path, 'info' => ['help'] %>
|
41
|
+
<% end %>
|
42
|
+
</ul>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
<div id="main">
|
46
|
+
<div class="wrap">
|
47
|
+
<% if flash[:notice] %><div class="notice"><%=simple_format h(flash[:notice]) %></div><% end %>
|
48
|
+
<% if flash[:alert] %>
|
49
|
+
<div class="error">
|
50
|
+
<%=simple_format h(flash[:alert]) %>
|
51
|
+
|
52
|
+
<% unless params[:resend_activation_for].blank? -%>
|
53
|
+
<%= button_to t(:resend_activation_email), resend_activation_email_path(:account => params[:resend_activation_for]) -%>
|
54
|
+
<%- end %>
|
55
|
+
</div>
|
56
|
+
<% end %>
|
57
|
+
<%= yield %>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<div id="foot">
|
61
|
+
<div class="wrap">
|
62
|
+
<span class="note">
|
63
|
+
powered by <%= link_to 'masq', 'https://github.com/oauth-xx/masq2' %>
|
64
|
+
and <%= link_to image_tag('masq/openid_symbol.png') + " OpenID", 'https://openid.net/' %> |
|
65
|
+
<%= link_to t(:get_help), help_path %>
|
66
|
+
</span>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
</body>
|
70
|
+
</html>
|