web47sso 0.1.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/.bundle/config +3 -0
- data/.circleci/config.yml +172 -0
- data/.gitignore +59 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +440 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +10 -0
- data/app/assets/config/manifest.js +3 -0
- data/app/controllers/sso_google_servers_controller.rb +44 -0
- data/app/jobs/cron/trim_secure_requests.rb +20 -0
- data/app/views/sso_google_servers/_form.html.haml +16 -0
- data/app/views/sso_google_servers/edit.html.haml +10 -0
- data/app/views/sso_google_servers/new.html.haml +9 -0
- data/app/views/sso_servers/index.html.haml +28 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/locales/en.yml +5 -0
- data/coverage_merge.rb +28 -0
- data/lib/app/controllers/concerns/core_sso_servers_controller.rb +58 -0
- data/lib/app/models/secure_request.rb +73 -0
- data/lib/app/models/sso_google_server.rb +28 -0
- data/lib/app/models/sso_oauth_server.rb +212 -0
- data/lib/app/models/sso_oauth_token.rb +22 -0
- data/lib/app/models/sso_server.rb +61 -0
- data/lib/app/models/sso_user_login_request.rb +11 -0
- data/lib/app/models/user_login_request.rb +15 -0
- data/lib/app/models/user_request.rb +22 -0
- data/lib/web47sso/version.rb +3 -0
- data/lib/web47sso.rb +16 -0
- data/web47sso.gemspec +61 -0
- metadata +406 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "web47sso"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/coverage_merge.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
require 'simplecov_lcov_formatter'
|
5
|
+
|
6
|
+
# Load up the files in the workspace and merge or collate them together.
|
7
|
+
# This should kick out a single coverage file out
|
8
|
+
SimpleCov::Formatter::LcovFormatter.config do |c|
|
9
|
+
c.single_report_path = 'coverage/final.lcov'
|
10
|
+
c.report_with_single_file = true
|
11
|
+
end
|
12
|
+
SimpleCov.collate Dir['/tmp/workspace/lcov/*.json'], 'rails' do
|
13
|
+
add_filter '/spec/'
|
14
|
+
add_filter '/config/'
|
15
|
+
add_filter '/lib/'
|
16
|
+
add_filter '/test/'
|
17
|
+
add_filter '/vendor/'
|
18
|
+
add_filter '/config/'
|
19
|
+
add_filter '/db/'
|
20
|
+
add_filter '/public/'
|
21
|
+
add_filter '/coverage_merge.rb'
|
22
|
+
add_filter '/Gemfile'
|
23
|
+
add_filter '/Rakefile'
|
24
|
+
add_filter '/Gemfile.lock'
|
25
|
+
add_filter '/rubocop.yml'
|
26
|
+
enable_coverage :branch
|
27
|
+
formatter SimpleCov::Formatter::LcovFormatter
|
28
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Manage SSO severs
|
5
|
+
#
|
6
|
+
module CoreSsoServersController
|
7
|
+
include CoreController
|
8
|
+
include App47Logger
|
9
|
+
|
10
|
+
def index
|
11
|
+
@sso_servers = SsoServer.asc(:name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def update
|
15
|
+
sso_server.update_attributes_and_log!(current_user, sso_server_params)
|
16
|
+
flash[:notice] = 'SSO Server updated'
|
17
|
+
redirect_to_referrer sso_servers_path
|
18
|
+
rescue StandardError => error
|
19
|
+
log_controller_error error
|
20
|
+
render :edit
|
21
|
+
end
|
22
|
+
|
23
|
+
def create
|
24
|
+
sso_server.save_and_log! current_user, sso_server_params
|
25
|
+
flash[:notice] = 'SSO Server created'
|
26
|
+
redirect_to_referrer sso_servers_path
|
27
|
+
rescue StandardError => error
|
28
|
+
log_controller_error error
|
29
|
+
render :new
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy
|
33
|
+
sso_server.destroy_and_log current_user
|
34
|
+
flash[:notice] = 'SSO Server removed'
|
35
|
+
redirect_to_referrer sso_servers_path
|
36
|
+
rescue StandardError => error
|
37
|
+
log_controller_error error, true
|
38
|
+
redirect_to_referrer sso_servers_path
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def sso_server
|
44
|
+
@sso_server
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Parameters
|
49
|
+
#
|
50
|
+
def sso_server_params
|
51
|
+
params[:sso_server][:active] ||= 'off'
|
52
|
+
params[:sso_server].permit(allowed_param_names)
|
53
|
+
end
|
54
|
+
|
55
|
+
def allowed_param_names
|
56
|
+
SsoServer.allowed_param_names
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Capture a secure request transaction
|
5
|
+
#
|
6
|
+
class SecureRequest
|
7
|
+
include StandardModel
|
8
|
+
#
|
9
|
+
# Fields
|
10
|
+
#
|
11
|
+
field :code, { type: String }
|
12
|
+
field :used, { type: Boolean, default: false }
|
13
|
+
field :ip_address, { type: String }
|
14
|
+
field :user_agent, { type: String }
|
15
|
+
field :valid_duration_hrs, { type: Integer, default: 1 }
|
16
|
+
# Make consumed and alias synonyms
|
17
|
+
alias consumed? used
|
18
|
+
# Make state and ID the same
|
19
|
+
alias state id
|
20
|
+
|
21
|
+
# @abstract Consume the requests, but also verify it's valid before doing so
|
22
|
+
# @param [Hash] options - options to update based on request
|
23
|
+
def consume!(options = {})
|
24
|
+
raise 'Expired request' if expired?
|
25
|
+
raise 'Consumed request' if consumed?
|
26
|
+
|
27
|
+
consume(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @@abstract Mark this request as consumed recording the user and request information if given
|
31
|
+
# @param [Hash] options - options to update based on request
|
32
|
+
def consume(options = {})
|
33
|
+
self.used = true
|
34
|
+
if options[:request].present?
|
35
|
+
self.ip_address = options[:request].remote_ip
|
36
|
+
self.user_agent = options[:request].user_agent
|
37
|
+
end
|
38
|
+
save!
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# What is the current status?
|
43
|
+
#
|
44
|
+
def status
|
45
|
+
if expired?
|
46
|
+
'Expired'
|
47
|
+
elsif consumed?
|
48
|
+
'Consumed'
|
49
|
+
else
|
50
|
+
'New'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @abstract Check if expired or used
|
55
|
+
# @return Boolean - True if we are good to go, false otherwise
|
56
|
+
def good?
|
57
|
+
!consumed? && not_expired?
|
58
|
+
end
|
59
|
+
|
60
|
+
# @abstract Check if expired
|
61
|
+
# @return Boolean - True if we are good to go, false otherwise
|
62
|
+
def expired?
|
63
|
+
created_at < valid_duration_hrs.hours.ago.utc
|
64
|
+
rescue StandardError
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
# @abstract Check if not expired
|
69
|
+
# @return Boolean
|
70
|
+
def not_expired?
|
71
|
+
!expired?
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Google OAuth 2.0
|
5
|
+
#
|
6
|
+
class SsoGoogleServer < SsoOauthServer
|
7
|
+
#
|
8
|
+
# Google Server SSO name
|
9
|
+
#
|
10
|
+
def display_name
|
11
|
+
'Google OAuth 2.0'
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
#
|
17
|
+
# Google SSO settings
|
18
|
+
#
|
19
|
+
def customize_defaults
|
20
|
+
self.server_url ||= 'https://accounts.google.com'
|
21
|
+
self.auth_url ||= 'https://accounts.google.com/o/oauth2/auth'
|
22
|
+
self.token_url ||= 'https://accounts.google.com/o/oauth2/token'
|
23
|
+
self.profile_url ||= 'https://www.googleapis.com/userinfo/v2/me'
|
24
|
+
self.redirect_path ||= 'auth/google'
|
25
|
+
self.scopes ||= 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# OAuth 2.0 Server
|
5
|
+
#
|
6
|
+
class SsoOauthServer < SsoServer
|
7
|
+
# Fields
|
8
|
+
field :server_url, type: String
|
9
|
+
field :auth_url, type: String
|
10
|
+
field :token_url, type: String
|
11
|
+
field :profile_url, type: String
|
12
|
+
field :redirect_path, type: String
|
13
|
+
field :client_id, type: String
|
14
|
+
field :client_secret, type: String
|
15
|
+
field :scopes, type: String
|
16
|
+
# Validations
|
17
|
+
validates :client_id, presence: true
|
18
|
+
validates :client_secret, presence: true
|
19
|
+
|
20
|
+
# @abstract Using the appropriate SSO server configuration obtain the user profile address
|
21
|
+
# @return Hash - User profile as a hash
|
22
|
+
def user_profile(code)
|
23
|
+
# Exchange the code to validate the token
|
24
|
+
token = exchange(SystemConfiguration.base_url, code)
|
25
|
+
log_debug "user_profile token: #{token.inspect}"
|
26
|
+
raise 'Invalid token' unless token.present? && token.valid?
|
27
|
+
|
28
|
+
# Get scoped information using the access token
|
29
|
+
user_profile = get_profile(token)
|
30
|
+
log_debug "user_profile user_profile: #{user_profile.inspect}"
|
31
|
+
raise 'Failed to retrieve user profile from provider' if user_profile.blank?
|
32
|
+
|
33
|
+
# user_profile.symbolize_keys
|
34
|
+
clean_user_profile(user_profile)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @abstract Default mapping of user profile keys to what the caller is expecting
|
38
|
+
# @return Hash - Mapping of key names for the user profile
|
39
|
+
def profile_key_mapping
|
40
|
+
@profile_key_mapping ||= { name: 'name', email: 'email' }
|
41
|
+
end
|
42
|
+
|
43
|
+
# @abstract Clean up the user profile to only return what is listed in the key mapping. Not everything
|
44
|
+
# @return Hash - The user profile using the mapping from this class
|
45
|
+
def clean_user_profile(user_profile)
|
46
|
+
profile = {}
|
47
|
+
profile_key_mapping.each do |key, value|
|
48
|
+
profile[key] = user_profile[value]
|
49
|
+
end
|
50
|
+
profile
|
51
|
+
end
|
52
|
+
|
53
|
+
# @abstract OAuth 2.0 provider's authorization endpoint URL
|
54
|
+
# @return URL - The URL for the authorization end point
|
55
|
+
def auth_endpoint
|
56
|
+
return nil if server_url.blank? || auth_url.blank?
|
57
|
+
|
58
|
+
prefix_server auth_url
|
59
|
+
end
|
60
|
+
|
61
|
+
# @abstract OAuth 2.0 provider's token endpoint URL
|
62
|
+
# @return URL - the Token endpoint
|
63
|
+
def token_endpoint
|
64
|
+
return nil if server_url.blank? || token_url.blank?
|
65
|
+
|
66
|
+
prefix_server token_url
|
67
|
+
end
|
68
|
+
|
69
|
+
# @abstract OAuth 2.0 provider's profile endpoint URL
|
70
|
+
# @return Profile end point
|
71
|
+
def profile_endpoint
|
72
|
+
return nil if server_url.blank? || profile_url.blank?
|
73
|
+
|
74
|
+
prefix_server profile_url
|
75
|
+
end
|
76
|
+
|
77
|
+
# @abstract URL to redirect users to going through the flow
|
78
|
+
# @return URL - Redirect URL for this server
|
79
|
+
def redirect_uri(root_url = SystemConfiguration.base_url)
|
80
|
+
return root_url if redirect_path.blank? || root_url.blank?
|
81
|
+
|
82
|
+
[root_url, (root_url.end_with?('/') ? '' : '/'), redirect_path.gsub(%r{^/+}, '')].join
|
83
|
+
end
|
84
|
+
|
85
|
+
# @abstract Scope specifies optional requested permissions
|
86
|
+
# @return Array[String] - Scopes for this server
|
87
|
+
def requested_scopes
|
88
|
+
scopes.present? ? scopes.split(' ') : []
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Returns a URL to the OAuth 2.0 provider's consent page
|
93
|
+
# asking for permissions for the given scopes explicitly.
|
94
|
+
#
|
95
|
+
# State is a token to protect the user from CSRF attacks. You
|
96
|
+
# must always provide a non-zero string and validate that it
|
97
|
+
# matches the state query parameter on your redirect callback.
|
98
|
+
#
|
99
|
+
def auth_code_url(request, parameters = {})
|
100
|
+
super
|
101
|
+
uri = URI(auth_endpoint)
|
102
|
+
uri.query = parameters.merge(response_type: 'code',
|
103
|
+
client_id: client_id,
|
104
|
+
redirect_uri: redirect_uri,
|
105
|
+
scope: requested_scopes.join(' '),
|
106
|
+
state: request.state).to_query
|
107
|
+
uri.to_s
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Exchange will convert an authorization code into a token.
|
112
|
+
#
|
113
|
+
def exchange(root_url, code)
|
114
|
+
response = send_request_for_exchange(root_url, code)
|
115
|
+
log_debug response.body.inspect
|
116
|
+
raise 'Cannot fetch token' unless response.code.between? 200, 299
|
117
|
+
|
118
|
+
extract_token(response_values(response))
|
119
|
+
rescue StandardError => error
|
120
|
+
log_error 'Unable to retrieve profile', error
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# @abstract Get the profile given an access token, can be overriden by concrete implementations.
|
125
|
+
# @param [SsoOauthToken] access_token
|
126
|
+
# @return [Hash]
|
127
|
+
def get_profile(access_token)
|
128
|
+
log_debug "get_profile profile_endpoint: #{profile_endpoint} with token: #{access_token.inspect}"
|
129
|
+
response = RestClient.get(profile_endpoint, Authorization: access_token.authorization)
|
130
|
+
log_debug "get_profile response: #{response.body}"
|
131
|
+
response_values(response)
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Based on the response content type get a hash of values.
|
136
|
+
#
|
137
|
+
def response_values(response)
|
138
|
+
log_debug "response_values response: #{response.inspect}"
|
139
|
+
case response.headers[:content_type]
|
140
|
+
when 'application/x-www-form-urlencoded', 'text/plain'
|
141
|
+
log_debug "form or plain text response: #{response.body}"
|
142
|
+
log_debug Rack::Utils.parse_nested_query(response.body).inspect
|
143
|
+
Rack::Utils.parse_nested_query(response.body)
|
144
|
+
else
|
145
|
+
log_debug "json: #{response.body}"
|
146
|
+
JSON.parse(response.body)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
#
|
151
|
+
# Token will come in the following schema (hopefully)
|
152
|
+
#
|
153
|
+
# - AccessToken: access_token
|
154
|
+
# - TokenType: token_type
|
155
|
+
# - Refresh Token: refresh_token
|
156
|
+
# - Expiry: expires_in, expires (thank you FB)
|
157
|
+
#
|
158
|
+
def extract_token(vals)
|
159
|
+
log_debug "extract_token: #{vals.inspect}"
|
160
|
+
token = SsoOauthToken.new
|
161
|
+
token.access_token = vals['access_token']
|
162
|
+
token.token_type = vals['token_type']
|
163
|
+
token.refresh_token = vals['refresh_token']
|
164
|
+
|
165
|
+
# Convert `e` to integer and then to a time
|
166
|
+
e = vals['expires_in']
|
167
|
+
e = vals['expires'] if e.blank?
|
168
|
+
token.expiry = Time.now.utc + e.to_i.seconds unless e.blank?
|
169
|
+
|
170
|
+
token
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
#
|
176
|
+
# If a relative path was given prepend the server as the base
|
177
|
+
#
|
178
|
+
def prefix_server(path)
|
179
|
+
if path.start_with?(server_url) || path.starts_with?('http')
|
180
|
+
path
|
181
|
+
else
|
182
|
+
"#{server_url.chomp('/')}/#{path.gsub(%r{^/+}, '')}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Send HTTP POST to exchange the authorization code for an access token.
|
188
|
+
#
|
189
|
+
def send_request_for_exchange(root_url, code)
|
190
|
+
RestClient.post(token_endpoint,
|
191
|
+
{
|
192
|
+
grant_type: 'authorization_code',
|
193
|
+
client_id: client_id,
|
194
|
+
client_secret: client_secret,
|
195
|
+
code: code,
|
196
|
+
redirect_uri: redirect_uri(root_url),
|
197
|
+
scope: requested_scopes.join(' ')
|
198
|
+
},
|
199
|
+
content_type: 'application/x-www-form-urlencoded',
|
200
|
+
accept: :json)
|
201
|
+
rescue RestClient::ExceptionWithResponse => error
|
202
|
+
log_error "Unable to fetch OAuth token #{inspect}, #{response.inspect}", error
|
203
|
+
log_error "Redirect_uri #{redirect_uri(root_url)}"
|
204
|
+
log_error "response body #{response.body}"
|
205
|
+
raise error
|
206
|
+
end
|
207
|
+
|
208
|
+
def customize_defaults
|
209
|
+
self.redirect_path ||= 'auth/oauth'
|
210
|
+
super
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# OAuth 2.0 Token
|
5
|
+
#
|
6
|
+
class SsoOauthToken
|
7
|
+
attr_accessor :access_token, :token_type, :refresh_token, :expiry
|
8
|
+
|
9
|
+
# @abstract The authorization string for this token type
|
10
|
+
# @return String
|
11
|
+
def authorization
|
12
|
+
[type, access_token].compact.join(' ')
|
13
|
+
end
|
14
|
+
|
15
|
+
def type
|
16
|
+
@token_type.presence || 'Bearer'
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
@access_token.present? && (@expiry.nil? || Time.now.utc < @expiry)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Base server container for an account which can have 0..N sso servers configured.
|
5
|
+
#
|
6
|
+
class SsoServer
|
7
|
+
include StandardModel
|
8
|
+
include SearchAble
|
9
|
+
#
|
10
|
+
# fields
|
11
|
+
#
|
12
|
+
field :active, type: Boolean, default: false
|
13
|
+
field :name, type: String
|
14
|
+
field :logout_url, type: String
|
15
|
+
#
|
16
|
+
# relationships
|
17
|
+
#
|
18
|
+
has_many :users, inverse_of: :sso_server, dependent: :nullify, class_name: 'User'
|
19
|
+
has_many :requests, inverse_of: :sso_server, class_name: 'SsoUserLoginRequest', dependent: :destroy
|
20
|
+
#
|
21
|
+
# Validations
|
22
|
+
#
|
23
|
+
validates :name, presence: true, uniqueness: true
|
24
|
+
validates :logout_url, url: true, allow_blank: true
|
25
|
+
#
|
26
|
+
# callbacks
|
27
|
+
#
|
28
|
+
after_initialize :customize_defaults
|
29
|
+
|
30
|
+
# @abstract Default is to do nothing, but subclasses may override this
|
31
|
+
def customize_defaults; end
|
32
|
+
|
33
|
+
# @abstract Display name for this server, should be overridden by concrete implementations.
|
34
|
+
# @return String
|
35
|
+
def display_name
|
36
|
+
'SSO Server'
|
37
|
+
end
|
38
|
+
|
39
|
+
# @abstract Return the welcome message
|
40
|
+
# @return String
|
41
|
+
def display_instructions
|
42
|
+
"Click to get started with #{display_name}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# @abstract Using the appropriate SSO server configuration obtain the
|
46
|
+
# user profile address, this should be implemented by the child class.
|
47
|
+
# @return Hash
|
48
|
+
# @raise NotImplementedError if the method is not implemented in the concrete class
|
49
|
+
def user_profile(_code)
|
50
|
+
raise NotImplementedError, 'Failed to retrieve user profile from provider'
|
51
|
+
end
|
52
|
+
|
53
|
+
# @abstract Get the authentication URL for the SSO Server, must be implemented by the concrete class
|
54
|
+
# @return URL
|
55
|
+
def auth_code_url(request, _parameters = {})
|
56
|
+
return if request.is_a?(SsoUserLoginRequest)
|
57
|
+
|
58
|
+
request = request.becomes(SsoUserLoginRequest)
|
59
|
+
request.update! sso_server: self
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Capture the request of a User authentication
|
5
|
+
#
|
6
|
+
class UserLoginRequest < UserRequest
|
7
|
+
#
|
8
|
+
# Fields
|
9
|
+
#
|
10
|
+
field :browser_uid, { type: String }
|
11
|
+
#
|
12
|
+
# Validations
|
13
|
+
#
|
14
|
+
validates :browser_uid, presence: true
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Capture the request for a user
|
5
|
+
#
|
6
|
+
class UserRequest < SecureRequest
|
7
|
+
#
|
8
|
+
# Relationships
|
9
|
+
#
|
10
|
+
belongs_to :user, { inverse_of: :requests, optional: true, class_name: 'User' }
|
11
|
+
|
12
|
+
# @abstract Mark this request as consumed recording the user and request information if given
|
13
|
+
def consume(options = {})
|
14
|
+
self.user = options[:user] if options[:user].present?
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @abstract Return the end point for the given request
|
19
|
+
def default_uri(endpoint_path = 'home')
|
20
|
+
[SystemConfiguration.base_url, endpoint_path].join('/')
|
21
|
+
end
|
22
|
+
end
|
data/lib/web47sso.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "web47sso/version"
|
2
|
+
require 'app/models/secure_request'
|
3
|
+
require 'app/models/sso_server'
|
4
|
+
require 'app/models/sso_oauth_server'
|
5
|
+
require 'app/models/sso_google_server'
|
6
|
+
require 'app/models/sso_oauth_token'
|
7
|
+
require 'app/models/user_request'
|
8
|
+
require 'app/models/user_login_request'
|
9
|
+
require 'app/models/sso_user_login_request'
|
10
|
+
|
11
|
+
require 'app/controllers/concerns/core_sso_servers_controller'
|
12
|
+
|
13
|
+
module Web47sso
|
14
|
+
class Error < StandardError; end
|
15
|
+
# Your code goes here...
|
16
|
+
end
|
data/web47sso.gemspec
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "web47sso/version"
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "web47sso"
|
7
|
+
spec.version = Web47sso::VERSION
|
8
|
+
spec.authors = ["Chris Schroeder"]
|
9
|
+
spec.email = ["chris@app47.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{App47 Web SSO}
|
12
|
+
spec.description = %q{Single Sign On (SSO) components used in several App47 Apps}
|
13
|
+
spec.homepage = "https://github.com/App47/web47sso.git"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata["allowed_push_host"] = 'https://rubygems.org'
|
20
|
+
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
22
|
+
spec.metadata["source_code_uri"] = "https://github.com/App47/web47sso.git"
|
23
|
+
else
|
24
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
25
|
+
"public gem pushes."
|
26
|
+
end
|
27
|
+
|
28
|
+
# Specify which files should be added to the gem when it is released.
|
29
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
30
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
31
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
32
|
+
end
|
33
|
+
spec.bindir = "bin"
|
34
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
35
|
+
spec.require_paths = ["lib"]
|
36
|
+
|
37
|
+
spec.add_runtime_dependency 'mongoid', '~> 9.0'
|
38
|
+
|
39
|
+
spec.add_runtime_dependency 'web47core', '~> 3.2.28'
|
40
|
+
spec.add_development_dependency "rake"
|
41
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
42
|
+
spec.add_development_dependency 'brakeman'
|
43
|
+
spec.add_development_dependency 'codacy-coverage'
|
44
|
+
spec.add_development_dependency 'database_cleaner-mongoid'
|
45
|
+
spec.add_development_dependency 'factory_bot'
|
46
|
+
spec.add_development_dependency 'factory_bot_rails'
|
47
|
+
spec.add_development_dependency 'listen'
|
48
|
+
spec.add_development_dependency 'minitest-rails'
|
49
|
+
spec.add_development_dependency 'minitest-reporters'
|
50
|
+
spec.add_development_dependency 'mocha'
|
51
|
+
spec.add_development_dependency 'rails', '~> 7.2.2'
|
52
|
+
spec.add_development_dependency 'railties'
|
53
|
+
spec.add_development_dependency 'sass-rails'
|
54
|
+
spec.add_development_dependency 'shoulda', '~> 4.0.0'
|
55
|
+
spec.add_development_dependency 'shoulda-context'
|
56
|
+
spec.add_development_dependency 'shoulda-matchers'
|
57
|
+
spec.add_development_dependency 'simplecov'
|
58
|
+
spec.add_development_dependency 'simplecov_lcov_formatter'
|
59
|
+
spec.add_development_dependency 'test-unit'
|
60
|
+
spec.add_development_dependency 'webmock'
|
61
|
+
end
|