anoubis_sso_server 0.1.1 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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