anoubis_sso_server 0.1.1 → 1.0.5
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 +4 -4
- data/.env.sample +3 -0
- data/.rspec +2 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +8 -2
- data/Gemfile +15 -5
- data/README.md +91 -4
- data/Rakefile +3 -0
- data/app/controllers/anoubis_sso_server/application_controller.rb +199 -3
- data/app/controllers/anoubis_sso_server/data_controller.rb +5 -0
- data/app/controllers/anoubis_sso_server/index_controller.rb +50 -0
- data/app/controllers/anoubis_sso_server/main_controller.rb +126 -0
- data/app/controllers/anoubis_sso_server/open_id_controller.rb +373 -0
- data/app/models/anoubis_sso_server/application_record.rb +4 -1
- data/app/models/anoubis_sso_server/system.rb +68 -0
- data/app/models/anoubis_sso_server/user.rb +57 -17
- data/config/locales/en.yml +11 -0
- data/config/locales/ru.yml +11 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20220214112633_create_anoubis_sso_server_users.rb +19 -0
- data/db/migrate/20220214112748_create_anoubis_sso_server_systems.rb +17 -0
- data/lib/anoubis_sso_server/engine.rb +1 -1
- data/lib/anoubis_sso_server/version.rb +1 -1
- data/sig/anoubis_sso_server.rbs +6 -0
- metadata +207 -12
- data/.idea/.gitignore +0 -6
- data/.idea/anoubis_sso_server.iml +0 -84
- data/.idea/inspectionProfiles/Project_Default.xml +0 -6
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
- data/Gemfile.lock +0 -217
@@ -0,0 +1,373 @@
|
|
1
|
+
##
|
2
|
+
# OpenID controller class. Defines any OpenID actions according by specification.
|
3
|
+
class AnoubisSsoServer::OpenIdController < AnoubisSsoServer::ApplicationController
|
4
|
+
|
5
|
+
##
|
6
|
+
# Action returns {https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse Provider OpenID configuration}.
|
7
|
+
#
|
8
|
+
# Default path: /openid/.well-known/openid-configuration
|
9
|
+
# @return [Hash] Current OpenID configuration
|
10
|
+
def configuration
|
11
|
+
result = {
|
12
|
+
issuer: sso_server + 'openid/',
|
13
|
+
authorization_endpoint: sso_server + 'openid/oauth2/auth',
|
14
|
+
token_endpoint: sso_server + 'openid/oauth2/token',
|
15
|
+
jwks_uri: sso_server + 'openid/.well-known/jwks.json',
|
16
|
+
subject_types_supported: %w[public],
|
17
|
+
#response_types_supported: ['code', 'code id_token', 'id_token', 'token id_token', 'token', 'token id_token code'],
|
18
|
+
response_types_supported: %w[code],
|
19
|
+
claims_supported: %w[sub],
|
20
|
+
#grant_types_supported: ['authorization_code', 'implicit', 'client_credentials', 'refresh_token'],
|
21
|
+
grant_types_supported: %w[authorization_code],
|
22
|
+
response_modes_supported: %w[query fragment],
|
23
|
+
userinfo_endpoint: sso_server + 'openid/userinfo',
|
24
|
+
scopes_supported: %w[offline_access offline openid'],
|
25
|
+
token_endpoint_auth_methods_supported: %w[client_secret_post client_secret_basic private_key_jwt none],
|
26
|
+
userinfo_signing_alg_values_supported: %w[none RS256],
|
27
|
+
id_token_signing_alg_values_supported: %w[RS256],
|
28
|
+
request_parameter_supported: true,
|
29
|
+
request_uri_parameter_supported: true,
|
30
|
+
require_request_uri_registration: true,
|
31
|
+
claims_parameter_supported: false,
|
32
|
+
revocation_endpoint: sso_server + 'openid/oauth2/revoke',
|
33
|
+
backchannel_logout_supported: true,
|
34
|
+
backchannel_logout_session_supported: true,
|
35
|
+
frontchannel_logout_supported: true,
|
36
|
+
frontchannel_logout_session_supported: true,
|
37
|
+
end_session_endpoint: sso_server + 'openid/oauth2/sessions/logout',
|
38
|
+
request_object_signing_alg_values_supported: %w[RS256 none],
|
39
|
+
code_challenge_methods_supported: %w[plain S256]
|
40
|
+
}
|
41
|
+
|
42
|
+
render json: result
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Action returns OpenID JWKs.
|
47
|
+
#
|
48
|
+
# Default path: /openid/.well-known/jwks.json
|
49
|
+
# @return [Hash] Current JWKs
|
50
|
+
def jwks
|
51
|
+
begin
|
52
|
+
jwks_cache = JSON.parse(self.redis.get("#{redis_prefix}jwks"),{ symbolize_names: true })
|
53
|
+
rescue StandardError => e
|
54
|
+
jwks_cache = generate_jwks
|
55
|
+
end
|
56
|
+
|
57
|
+
redis.set "#{redis_prefix}jwks", jwks_cache.to_json, ex: 3600
|
58
|
+
|
59
|
+
render json: jwks_cache
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Action for check user authorization for current browser.
|
64
|
+
def auth
|
65
|
+
result = {
|
66
|
+
result: -1
|
67
|
+
}
|
68
|
+
|
69
|
+
params[:prompt] = 'is' unless params.key? :prompt
|
70
|
+
params[:prompt] = 'is' if params[:prompt] != 'none'
|
71
|
+
|
72
|
+
err = check_basic_parameters
|
73
|
+
|
74
|
+
if err
|
75
|
+
result[:message] = err
|
76
|
+
return render(json: result)
|
77
|
+
end
|
78
|
+
|
79
|
+
sign = params[:redirect_uri].index('?') ? '&' : '?'
|
80
|
+
|
81
|
+
err = check_listed_parameters %w[response_type scope code_challenge code_challenge_method state]
|
82
|
+
|
83
|
+
if err
|
84
|
+
result[:message] = err
|
85
|
+
return if redirect_to_uri result[:message], sign
|
86
|
+
return render(json: result)
|
87
|
+
end
|
88
|
+
|
89
|
+
unless %w[code].include? params[:response_type]
|
90
|
+
result[:message] = I18n.t('anoubis.errors.is_not_correct', title: 'response_type')
|
91
|
+
return if redirect_to_uri result[:message], sign
|
92
|
+
return render(json: result)
|
93
|
+
end
|
94
|
+
|
95
|
+
scopes = params[:scope].split(' ')
|
96
|
+
|
97
|
+
params[:code_challenge_method] = params[:code_challenge_method].downcase
|
98
|
+
unless %w[s256].include? params[:code_challenge_method]
|
99
|
+
result[:message] = I18n.t('anoubis.errors.is_not_correct', title: 'code_challenge_method')
|
100
|
+
return if self.redirect_to_uri result[:message], sign
|
101
|
+
return render(json: result)
|
102
|
+
end
|
103
|
+
|
104
|
+
if params[:state].length < 6
|
105
|
+
result[:message] = I18n.t('anoubis.errors.less_than', title: 'state', size: 6)
|
106
|
+
return if self.redirect_to_uri result[:message], sign
|
107
|
+
return render(json: result)
|
108
|
+
end
|
109
|
+
|
110
|
+
original_url = request.url[8..]
|
111
|
+
original_url = original_url[(original_url.index('/') + 1)..]
|
112
|
+
|
113
|
+
code = SecureRandom.uuid
|
114
|
+
code_hash = {
|
115
|
+
scope: scopes,
|
116
|
+
code_challenge: params[:code_challenge],
|
117
|
+
request_uri: params[:redirect_uri],
|
118
|
+
state: params[:state],
|
119
|
+
client_id: params[:client_id],
|
120
|
+
original_url: sso_server + original_url
|
121
|
+
}
|
122
|
+
|
123
|
+
session = self.get_oauth_session
|
124
|
+
|
125
|
+
if session
|
126
|
+
user = get_user_by_uuid session[:uuid]
|
127
|
+
|
128
|
+
if user
|
129
|
+
code_hash[:uuid] = user.uuid
|
130
|
+
redis.set("#{redis_prefix}code:#{code}", code_hash.to_json, ex: 6000)
|
131
|
+
redirect_to "#{params[:redirect_uri]}#{sign}state=#{params[:state]}&scope=#{params[:scope]}&code=#{code}", { allow_other_host: true }
|
132
|
+
return
|
133
|
+
else
|
134
|
+
redis.del("#{redis_prefix}session:#{cookies[:oauth_session]}")
|
135
|
+
cookies[:oauth_session] = nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
result[:message] = I18n.t('anoubis.errors.login_required')
|
140
|
+
|
141
|
+
if params[:prompt] == 'none'
|
142
|
+
redirect_to params[:redirect_uri] + sign + 'error=' + ERB::Util.url_encode(result[:message]), { allow_other_host: true }
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
146
|
+
url = sso_login_url
|
147
|
+
url += url.index('?') ? '&' : '?'
|
148
|
+
redis.set("#{redis_prefix}login_code:#{code}", code_hash.to_json, ex: 3600)
|
149
|
+
redirect_to "#{url}code=#{code}", { allow_other_host: true }
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Action makes access token based on defined parameters
|
154
|
+
def access_token
|
155
|
+
result = {
|
156
|
+
result: -1
|
157
|
+
}
|
158
|
+
|
159
|
+
params[:prompt] == 'yes'
|
160
|
+
|
161
|
+
err = check_basic_parameters
|
162
|
+
|
163
|
+
if err
|
164
|
+
result[:message] = err
|
165
|
+
return render(json: result)
|
166
|
+
end
|
167
|
+
|
168
|
+
err = check_listed_parameters %w[scope code code_verifier grant_type]
|
169
|
+
|
170
|
+
if err
|
171
|
+
result[:message] = err
|
172
|
+
return render(json: result)
|
173
|
+
end
|
174
|
+
|
175
|
+
begin
|
176
|
+
code = JSON.parse(redis.get("#{redis_prefix}code:#{params[:code]}"),{ symbolize_names: true })
|
177
|
+
rescue
|
178
|
+
code = nil
|
179
|
+
end
|
180
|
+
|
181
|
+
if !code || code.class != Hash
|
182
|
+
result[:message] = I18n.t('anoubis.errors.is_not_correct', title: 'code')
|
183
|
+
return render(json: result)
|
184
|
+
end
|
185
|
+
|
186
|
+
str = Digest::SHA256.base64digest(params[:code_verifier]).tr("+/", "-_").tr("=", "")
|
187
|
+
|
188
|
+
if code[:code_challenge] != str
|
189
|
+
result[:message] = I18n.t('anoubis.errors.is_not_correct', title: 'code_verifier')
|
190
|
+
return render(json: result)
|
191
|
+
end
|
192
|
+
|
193
|
+
if code[:request_uri] != params[:redirect_uri]
|
194
|
+
result[:error] = I18n.t('anoubis.errors.is_not_correct', title: 'request_uri')
|
195
|
+
return render(json: result)
|
196
|
+
end
|
197
|
+
|
198
|
+
header = {
|
199
|
+
alg: "RS256",
|
200
|
+
kid: "public:#{current_system.uuid}",
|
201
|
+
typ: "JWT"
|
202
|
+
}
|
203
|
+
|
204
|
+
user = get_user_by_uuid code[:uuid]
|
205
|
+
|
206
|
+
payload = {
|
207
|
+
aud: [],
|
208
|
+
client_id: current_system.uuid,
|
209
|
+
exp: Time.now.utc.to_i + current_system.ttl,
|
210
|
+
ext: {},
|
211
|
+
iat: Time.now.utc.to_i,
|
212
|
+
nbf: Time.now.utc.to_i,
|
213
|
+
iss: "#{sso_server}openid/",
|
214
|
+
jti: SecureRandom.uuid,
|
215
|
+
sub: SecureRandom.uuid,
|
216
|
+
scp: []
|
217
|
+
}
|
218
|
+
|
219
|
+
keys = JWT::JWK.import(current_system.jwk)
|
220
|
+
|
221
|
+
user_payload = {
|
222
|
+
aud: [current_system.public],
|
223
|
+
auth_time: Time.now.utc.to_i,
|
224
|
+
exp: Time.now.utc.to_i + current_system.ttl,
|
225
|
+
iss: "#{sso_server}openid/",
|
226
|
+
jti: SecureRandom.uuid,
|
227
|
+
sid: SecureRandom.uuid,
|
228
|
+
sub: SecureRandom.uuid,
|
229
|
+
iat: Time.now.utc.to_i,
|
230
|
+
rat: Time.now.utc.to_i - 1
|
231
|
+
}
|
232
|
+
|
233
|
+
user_payload[:email] = user.email if code[:scope].include? 'email'
|
234
|
+
|
235
|
+
if code[:scope].include? 'profile'
|
236
|
+
user_payload[:name] = user.name
|
237
|
+
user_payload[:surname] = user.surname
|
238
|
+
end
|
239
|
+
|
240
|
+
result = {
|
241
|
+
access_token: JWT.encode(payload, keys.keypair, 'RS256', header),
|
242
|
+
expires_in: current_system.ttl,
|
243
|
+
scope: code[:scope],
|
244
|
+
token_type: 'bearer',
|
245
|
+
id_token: JWT.encode(user_payload, keys.keypair, 'RS256', header),
|
246
|
+
}
|
247
|
+
|
248
|
+
token_hash = {
|
249
|
+
uuid: user.uuid
|
250
|
+
}
|
251
|
+
|
252
|
+
redis.set("#{redis_prefix}token:#{result[:access_token]}", token_hash.to_json, ex: current_system.ttl)
|
253
|
+
redis.del("#{redis_prefix}code:#{params[:code]}")
|
254
|
+
|
255
|
+
options
|
256
|
+
|
257
|
+
render json: result
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# Clear default session
|
262
|
+
def logout
|
263
|
+
redis.del("#{redis_prefix}session:#{cookies[:oauth_session]}")
|
264
|
+
cookies[:oauth_session] = nil
|
265
|
+
redirect_to sso_login_url, { allow_other_host: true }
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# Action that returns user information parameters
|
270
|
+
def userinfo
|
271
|
+
auth_token = request.env.fetch('HTTP_AUTHORIZATION', '').scan(/Bearer (.*)$/).flatten.last
|
272
|
+
|
273
|
+
unless auth_token
|
274
|
+
render json: { error: I18n.t('anoubis.errors.access_not_allowed') }
|
275
|
+
return
|
276
|
+
end
|
277
|
+
|
278
|
+
begin
|
279
|
+
data = JSON.parse(redis.get("#{redis_prefix}token:#{auth_token}"), { symbolize_names: true })
|
280
|
+
rescue StandardError
|
281
|
+
data = nil
|
282
|
+
end
|
283
|
+
|
284
|
+
if data.class == Hash
|
285
|
+
data = nil unless data.key? :uuid
|
286
|
+
else
|
287
|
+
data = nil
|
288
|
+
end
|
289
|
+
|
290
|
+
if data
|
291
|
+
data = load_userinfo data[:uuid]
|
292
|
+
end
|
293
|
+
|
294
|
+
unless data
|
295
|
+
render json: { error: I18n.t('anoubis.errors.access_not_allowed') }
|
296
|
+
return
|
297
|
+
end
|
298
|
+
|
299
|
+
render json: data
|
300
|
+
end
|
301
|
+
|
302
|
+
##
|
303
|
+
# Load userinfo information from model and convert it into hash
|
304
|
+
# @param uuid [String] - User identifier
|
305
|
+
# @return [Hash] - User information
|
306
|
+
def load_userinfo(uuid)
|
307
|
+
data = user_model.where(uuid: uuid).first
|
308
|
+
|
309
|
+
return nil unless data
|
310
|
+
|
311
|
+
{
|
312
|
+
public: data.public,
|
313
|
+
email: data.email,
|
314
|
+
name: data.name,
|
315
|
+
surname: data.surname,
|
316
|
+
timezone: data.timezone,
|
317
|
+
locale: data.locale
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# Check basic oauth parameters (client_id, redirect_uri)
|
323
|
+
def check_basic_parameters
|
324
|
+
return I18n.t('anoubis.errors.is_not_defined', title: 'client_id') unless params.key? :client_id
|
325
|
+
|
326
|
+
@current_system = self.get_current_system params[:client_id]
|
327
|
+
|
328
|
+
return I18n.t('anoubis.errors.is_not_correct', title: 'client_id') unless current_system
|
329
|
+
|
330
|
+
return I18n.t('anoubis.errors.is_not_defined', title: 'redirect_uri') unless params.key? :redirect_uri
|
331
|
+
|
332
|
+
return I18n.t('anoubis.errors.is_not_correct', title: 'redirect_uri') unless current_system.request_uri.include? params[:redirect_uri]
|
333
|
+
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Check if page should be redirected to url
|
339
|
+
# @param error [String] Error message
|
340
|
+
# @param sign [String] Redirect url sign (? or &)
|
341
|
+
# @return [Boolean] return 'true' if page should be redirected
|
342
|
+
def redirect_to_uri(error, sign)
|
343
|
+
if params[:prompt] == 'none'
|
344
|
+
redirect_to params[:redirect_uri] + sign + 'error=' + ERB::Util.url_encode(error), { allow_other_host: true }
|
345
|
+
return true
|
346
|
+
end
|
347
|
+
|
348
|
+
false
|
349
|
+
end
|
350
|
+
|
351
|
+
##
|
352
|
+
# Procedure generates keys according by used systems. Data is loaded from {AnoubisSsoServer::System}.
|
353
|
+
# @return [Hash] Hash ow JWK keys
|
354
|
+
def generate_jwks
|
355
|
+
result = {
|
356
|
+
keys: []
|
357
|
+
}
|
358
|
+
|
359
|
+
AnoubisSsoServer::System.where(state: 'opened').each do |sys|
|
360
|
+
key = {
|
361
|
+
use: 'sig',
|
362
|
+
kty: sys.jwk[:kty],
|
363
|
+
kid: "public:#{sys.uuid}",
|
364
|
+
alg: 'RS256',
|
365
|
+
n: sys.jwk[:n],
|
366
|
+
e: sys.jwk[:e]
|
367
|
+
}
|
368
|
+
result[:keys].push key
|
369
|
+
end
|
370
|
+
|
371
|
+
result
|
372
|
+
end
|
373
|
+
end
|
@@ -1,2 +1,5 @@
|
|
1
|
-
|
1
|
+
##
|
2
|
+
# Main Active Record object inherited from {https://www.rubydoc.info/gems/anoubis/Anoubis/ApplicationRecord Anoubis::ApplicationRecord}
|
3
|
+
class AnoubisSsoServer::ApplicationRecord < Anoubis::ApplicationRecord
|
4
|
+
self.abstract_class = true
|
2
5
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
##
|
2
|
+
# Default System class
|
3
|
+
class AnoubisSsoServer::System < AnoubisSsoServer::ApplicationRecord
|
4
|
+
self.table_name = 'systems'
|
5
|
+
|
6
|
+
before_validation :before_validation_sso_server_system_on_create, on: :create
|
7
|
+
before_save :before_save_sso_server_system
|
8
|
+
after_save :after_save_sso_server_system
|
9
|
+
after_destroy :after_destroy_sso_server_system
|
10
|
+
|
11
|
+
validates :title, presence: true, length: { maximum: 100 }
|
12
|
+
validates :uuid, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
13
|
+
validates :public, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
14
|
+
|
15
|
+
enum state: { opened: 0, hidden: 1 }
|
16
|
+
|
17
|
+
##
|
18
|
+
# Fires before create system. Procedure generates public and private UUID and RSA keypair
|
19
|
+
def before_validation_sso_server_system_on_create
|
20
|
+
self.uuid = setup_private_system_id
|
21
|
+
self.public = setup_public_system_id unless public
|
22
|
+
self.request_uri = [] unless request_uri
|
23
|
+
|
24
|
+
keys = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
25
|
+
self.jwk = keys.export include_private: true
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Procedure setup private user identifier. Procedure can be redefined.
|
30
|
+
# @return [String] public user identifier
|
31
|
+
def setup_private_system_id
|
32
|
+
SecureRandom.uuid
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Procedure setup public user identifier. Used for open API. Procedure can be redefined.
|
37
|
+
# @return [String] public user identifier
|
38
|
+
def setup_public_system_id
|
39
|
+
SecureRandom.uuid
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Returns JWK information
|
44
|
+
# @return [Hash] JWK information
|
45
|
+
def jwk
|
46
|
+
@jwk ||= super.deep_symbolize_keys!
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Fires before System was saved to SQL database. Delete old system cache if public identifier was changed.
|
51
|
+
def before_save_sso_server_system
|
52
|
+
redis.del "#{redis_prefix}system:#{public_was}" if public_was && public != public_was
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Fires after System was saved to SQL database and setup system cache in Redis database for fast access.
|
57
|
+
def after_save_sso_server_system
|
58
|
+
redis.set("#{redis_prefix}system:#{public}", { uuid: uuid, public: public, request_uri: request_uri, jwk: jwk, ttl: ttl }.to_json )
|
59
|
+
redis.del "#{redis_prefix}jwks"
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Fires after System was destroyed. Clears all systems caches.
|
64
|
+
def after_destroy_sso_server_system
|
65
|
+
redis.del "#{redis_prefix}system:#{public}"
|
66
|
+
redis.del "#{redis_prefix}jwks"
|
67
|
+
end
|
68
|
+
end
|
@@ -1,13 +1,16 @@
|
|
1
|
-
|
1
|
+
##
|
2
|
+
# Default user class
|
3
|
+
class AnoubisSsoServer::User < AnoubisSsoServer::ApplicationRecord
|
2
4
|
self.table_name = 'users'
|
3
5
|
|
4
6
|
has_secure_password
|
5
7
|
validates :email, presence: true
|
6
8
|
|
7
|
-
before_validation :
|
8
|
-
before_save :
|
9
|
-
after_destroy :
|
9
|
+
before_validation :before_validation_sso_server_user_on_create, on: :create
|
10
|
+
before_save :before_save_sso_server_user
|
11
|
+
after_destroy :after_destroy_sso_server_user
|
10
12
|
|
13
|
+
## Regexp validation mask for email
|
11
14
|
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
|
12
15
|
|
13
16
|
validates :email, presence: true, length: { maximum: 50 },
|
@@ -25,46 +28,83 @@ class AnoubisSsoServer::User < ApplicationRecord
|
|
25
28
|
validates :uuid, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
26
29
|
validates :public, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
+
##
|
32
|
+
# Fires before create any User on the server. Procedure generates internal UUID and setup timezone to GMT.
|
33
|
+
# Public user identifier is generated also if not defined.
|
34
|
+
def before_validation_sso_server_user_on_create
|
35
|
+
self.uuid = setup_private_user_id
|
36
|
+
self.public = setup_public_user_id unless public
|
37
|
+
|
31
38
|
self.timezone = 'GMT' if !self.timezone
|
32
39
|
end
|
33
40
|
|
34
|
-
|
41
|
+
##
|
42
|
+
# Procedure setup private user identifier. Procedure can be redefined.
|
43
|
+
# @return [String] public user identifier
|
44
|
+
def setup_private_user_id
|
45
|
+
SecureRandom.uuid
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Procedure setup public user identifier. Used for open API. Procedure can be redefined.
|
50
|
+
# @return [String] public user identifier
|
51
|
+
def setup_public_user_id
|
52
|
+
SecureRandom.uuid
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Fires before save User data to database.
|
57
|
+
# Procedure setup email, timezone and call procedure for clear Redis cache data for the user.
|
58
|
+
def before_save_sso_server_user
|
35
59
|
self.timezone = 'GMT' if !self.timezone
|
36
60
|
self.email = self.email.downcase
|
37
61
|
self.clear_cache
|
38
62
|
end
|
39
63
|
|
40
|
-
|
41
|
-
|
64
|
+
##
|
65
|
+
# Fires before delete User from database.
|
66
|
+
# Procedure call procedure for clear Redis cache data for the user.
|
67
|
+
def after_destroy_sso_server_user
|
68
|
+
clear_cache
|
42
69
|
end
|
43
70
|
|
71
|
+
##
|
72
|
+
# Procedure saves cached User model data to Redis database for improve access speed.
|
44
73
|
def save_cache
|
45
|
-
|
74
|
+
redis.set("#{redis_prefix}user:#{uuid}", self.to_json(except: [:password_digest])) if redis
|
46
75
|
end
|
47
76
|
|
77
|
+
##
|
78
|
+
# Procedure clear cached User model data.
|
48
79
|
def clear_cache
|
49
|
-
|
80
|
+
redis.del("#{redis_prefix}user:#{uuid}") if redis
|
50
81
|
end
|
51
82
|
|
83
|
+
##
|
84
|
+
# Procedure checks if password was changed.
|
85
|
+
# @return [Boolean] return true if password was changed
|
52
86
|
def password_changed?
|
53
87
|
!password.blank?
|
54
88
|
end
|
55
89
|
|
56
|
-
|
90
|
+
##
|
91
|
+
# Procedure returns User model data from the cache or database. If user data isn't present in cache then call procedure that cache User model data.
|
92
|
+
# @param uuid [String] - User private UUID
|
93
|
+
# @return [Hash] User data
|
94
|
+
def self.load_cache(uuid)
|
57
95
|
begin
|
58
|
-
data = JSON.parse redis.get(
|
96
|
+
data = JSON.parse self.redis.get("#{self.redis_prefix}user:#{uuid}"), { symbolize_names: true }
|
59
97
|
rescue
|
60
98
|
data = nil
|
61
99
|
end
|
62
100
|
|
63
101
|
unless data
|
64
102
|
user = self.where(uuid: uuid).first
|
65
|
-
|
66
|
-
|
67
|
-
|
103
|
+
|
104
|
+
return nil unless user
|
105
|
+
|
106
|
+
user.save_cache
|
107
|
+
data = JSON.parse(user.to_json(except: [:password_digest]), { symbolize_names: true })
|
68
108
|
end
|
69
109
|
|
70
110
|
data
|
@@ -0,0 +1,11 @@
|
|
1
|
+
en:
|
2
|
+
anoubis:
|
3
|
+
errors:
|
4
|
+
system_not_defined: "SSO system is not defined in Rails.configuration.anoubis_sso_system"
|
5
|
+
is_not_defined: "%{title} isn't defined"
|
6
|
+
is_not_correct: "%{title} isn't correct"
|
7
|
+
less_than: "%{title} length should be %{size} or more symbols"
|
8
|
+
login_required: "Login required"
|
9
|
+
fields:
|
10
|
+
login: "Login not defined"
|
11
|
+
password: "Password not defined"
|
@@ -0,0 +1,11 @@
|
|
1
|
+
ru:
|
2
|
+
anoubis:
|
3
|
+
errors:
|
4
|
+
system_not_defined: "SSO система не определена в Rails.configuration.anoubis_sso_system"
|
5
|
+
is_not_defined: "Переменная %{title} не определена"
|
6
|
+
is_not_correct: "Переменная %{title} некорректна"
|
7
|
+
less_than: "Длина переменной %{title} должна быть %{size} или более символов"
|
8
|
+
login_required: "Требуется логин"
|
9
|
+
fields:
|
10
|
+
login: "Логин не задан"
|
11
|
+
password: "Пароль не задан"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
##
|
2
|
+
# Defines default routes
|
3
|
+
AnoubisSsoServer::Engine.routes.draw do
|
4
|
+
Rails.application.routes.draw do
|
5
|
+
scope path: 'api', defaults: { format: 'json' } do
|
6
|
+
scope path: ':version' do
|
7
|
+
get 'login', to: 'anoubis_sso_server/main#login', as: 'api_internal_login'
|
8
|
+
get 'auth', to: 'anoubis_sso_server/main#auth', as: 'api_internal_auth'
|
9
|
+
get 'dashboard', to: 'anoubis_sso_server/index#dashboard'
|
10
|
+
get 'menu', to: 'anoubis_sso_server/index#menu'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
scope path: 'openid', defaults: { format: 'json' } do
|
15
|
+
get '.well-known/openid-configuration', to: 'anoubis_sso_server/open_id#configuration', as: 'openid_configuration'
|
16
|
+
get '.well-known/jwks.json', to: 'anoubis_sso_server/open_id#jwks', as: 'openid_jwks'
|
17
|
+
get 'userinfo', to: 'anoubis_sso_server/open_id#userinfo', as: 'userinfo'
|
18
|
+
get 'oauth2/auth', to: 'anoubis_sso_server/open_id#auth', as: 'oauth_auth'
|
19
|
+
post 'oauth2/token', to: 'anoubis_sso_server/open_id#access_token', as: 'oauth_token'
|
20
|
+
options 'oauth2/token', to: 'anoubis_sso_server/application#options', as: nil
|
21
|
+
get 'oauth2/logout', to: 'anoubis_sso_server/open_id#logout', as: 'oauth_logout'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateAnoubisSsoServerUsers < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :users do |t|
|
4
|
+
t.string :email, limit: 100, null: false
|
5
|
+
t.string :name, limit: 100, null: false
|
6
|
+
t.string :surname, limit: 100, null: false
|
7
|
+
t.string :timezone, limit: 30, null: false
|
8
|
+
t.string :locale, limit: 10, null: false, default: 'ru-RU'
|
9
|
+
t.string :password_digest, limit: 60, null: false
|
10
|
+
t.string :uuid, limit: 40, null: false
|
11
|
+
t.string :public, limit: 40, null: false
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
add_index :users, [:email], unique: true
|
16
|
+
add_index :users, [:uuid], unique: true
|
17
|
+
add_index :users, [:public], unique: true
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateAnoubisSsoServerSystems < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :systems do |t|
|
4
|
+
t.string :title, limit: 100, null: false
|
5
|
+
t.string :uuid, limit: 40, null: false
|
6
|
+
t.string :public, limit: 40, null: false
|
7
|
+
t.integer :ttl, default: 3600, null: false
|
8
|
+
t.integer :state, default: 0, null: false
|
9
|
+
t.json :jwk
|
10
|
+
t.json :request_uri
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
add_index :systems, [:uuid], unique: true
|
15
|
+
add_index :systems, [:public], unique: true
|
16
|
+
end
|
17
|
+
end
|
data/sig/anoubis_sso_server.rbs
CHANGED