anoubis_sso_server 0.1.0 → 1.0.2
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 -3
- data/README.md +93 -6
- data/Rakefile +3 -0
- data/app/controllers/anoubis_sso_server/application_controller.rb +292 -0
- 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 +320 -0
- data/app/models/anoubis_sso_server/application_record.rb +5 -0
- data/app/models/anoubis_sso_server/system.rb +68 -0
- data/app/models/anoubis_sso_server/user.rb +112 -0
- data/config/locales/en.yml +14 -0
- data/config/locales/ru.yml +14 -0
- data/config/routes.rb +23 -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 +13 -0
- data/lib/anoubis_sso_server/version.rb +2 -1
- data/lib/anoubis_sso_server.rb +5 -4
- data/sig/anoubis_sso_server.rbs +6 -0
- metadata +216 -10
- data/.idea/.gitignore +0 -6
- data/.idea/anoubis_sso_server.iml +0 -17
- data/.idea/inspectionProfiles/Project_Default.xml +0 -6
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
@@ -0,0 +1,320 @@
|
|
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.public}",
|
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
|
+
self.redis.set("#{redis_prefix}token:#{result[:access_token]}", token_hash.to_json, ex: current_system.ttl)
|
253
|
+
self.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
|
+
# Check basic oauth parameters (client_id, redirect_uri)
|
270
|
+
def check_basic_parameters
|
271
|
+
return I18n.t('anoubis.errors.is_not_defined', title: 'client_id') unless params.key? :client_id
|
272
|
+
|
273
|
+
@current_system = self.get_current_system params[:client_id]
|
274
|
+
|
275
|
+
return I18n.t('anoubis.errors.is_not_correct', title: 'client_id') unless current_system
|
276
|
+
|
277
|
+
return I18n.t('anoubis.errors.is_not_defined', title: 'redirect_uri') unless params.key? :redirect_uri
|
278
|
+
|
279
|
+
return I18n.t('anoubis.errors.is_not_correct', title: 'redirect_uri') unless current_system.request_uri.include? params[:redirect_uri]
|
280
|
+
|
281
|
+
nil
|
282
|
+
end
|
283
|
+
|
284
|
+
##
|
285
|
+
# Check if page should be redirected to url
|
286
|
+
# @param error [String] Error message
|
287
|
+
# @param sign [String] Redirect url sign (? or &)
|
288
|
+
# @return [Boolean] return 'true' if page should be redirected
|
289
|
+
def redirect_to_uri(error, sign)
|
290
|
+
if params[:prompt] == 'none'
|
291
|
+
redirect_to params[:redirect_uri] + sign + 'error=' + ERB::Util.url_encode(error), { allow_other_host: true }
|
292
|
+
return true
|
293
|
+
end
|
294
|
+
|
295
|
+
false
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# Procedure generates keys according by used systems. Data is loaded from {AnoubisSsoServer::System}.
|
300
|
+
# @return [Hash] Hash ow JWK keys
|
301
|
+
def generate_jwks
|
302
|
+
result = {
|
303
|
+
keys: []
|
304
|
+
}
|
305
|
+
|
306
|
+
AnoubisSsoServer::System.where(state: 'opened').each do |sys|
|
307
|
+
key = {
|
308
|
+
use: 'sig',
|
309
|
+
kty: sys.jwk[:kty],
|
310
|
+
kid: "public:#{sys.uuid}",
|
311
|
+
alg: 'RS256',
|
312
|
+
n: sys.jwk[:n],
|
313
|
+
e: sys.jwk[:e]
|
314
|
+
}
|
315
|
+
result[:keys].push key
|
316
|
+
end
|
317
|
+
|
318
|
+
result
|
319
|
+
end
|
320
|
+
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
|
@@ -0,0 +1,112 @@
|
|
1
|
+
##
|
2
|
+
# Default user class
|
3
|
+
class AnoubisSsoServer::User < AnoubisSsoServer::ApplicationRecord
|
4
|
+
self.table_name = 'users'
|
5
|
+
|
6
|
+
has_secure_password
|
7
|
+
validates :email, presence: true
|
8
|
+
|
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
|
12
|
+
|
13
|
+
## Regexp validation mask for email
|
14
|
+
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
|
15
|
+
|
16
|
+
validates :email, presence: true, length: { maximum: 50 },
|
17
|
+
format: { with: VALID_EMAIL_REGEX },
|
18
|
+
uniqueness: { case_sensitive: true }
|
19
|
+
|
20
|
+
validates :name, presence: true, length: { maximum: 100 }
|
21
|
+
validates :surname, presence: true, length: { maximum: 100 }
|
22
|
+
|
23
|
+
validates :password, length: { in: 5..30 }, on: [:create]
|
24
|
+
validates :password, length: { in: 5..30 }, on: [:update], if: :password_changed?
|
25
|
+
validates :password_confirmation, length: { in: 5..30 }, on: [:create]
|
26
|
+
validates :password_confirmation, length: { in: 5..30 }, on: [:update], if: :password_changed?
|
27
|
+
|
28
|
+
validates :uuid, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
29
|
+
validates :public, presence: true, length: { maximum: 40 }, uniqueness: { case_sensitive: true }
|
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
|
+
|
38
|
+
self.timezone = 'GMT' if !self.timezone
|
39
|
+
end
|
40
|
+
|
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
|
59
|
+
self.timezone = 'GMT' if !self.timezone
|
60
|
+
self.email = self.email.downcase
|
61
|
+
self.clear_cache
|
62
|
+
end
|
63
|
+
|
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
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Procedure saves cached User model data to Redis database for improve access speed.
|
73
|
+
def save_cache
|
74
|
+
redis.set("#{redis_prefix}user:#{uuid}", self.to_json(except: [:password_digest])) if redis
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Procedure clear cached User model data.
|
79
|
+
def clear_cache
|
80
|
+
redis.del("#{redis_prefix}user:#{uuid}") if redis
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Procedure checks if password was changed.
|
85
|
+
# @return [Boolean] return true if password was changed
|
86
|
+
def password_changed?
|
87
|
+
!password.blank?
|
88
|
+
end
|
89
|
+
|
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)
|
95
|
+
begin
|
96
|
+
data = JSON.parse self.redis.get("#{self.redis_prefix}user:#{uuid}"), { symbolize_names: true }
|
97
|
+
rescue
|
98
|
+
data = nil
|
99
|
+
end
|
100
|
+
|
101
|
+
unless data
|
102
|
+
user = self.where(uuid: uuid).first
|
103
|
+
|
104
|
+
return nil unless user
|
105
|
+
|
106
|
+
user.save_cache
|
107
|
+
data = JSON.parse(user.to_json(except: [:password_digest]), { symbolize_names: true })
|
108
|
+
end
|
109
|
+
|
110
|
+
data
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
en:
|
2
|
+
anoubis:
|
3
|
+
errors:
|
4
|
+
incorrect_login: "Incorrect login or password"
|
5
|
+
system_not_defined: "SSO system is not defined in Rails.configuration.anoubis_sso_system"
|
6
|
+
session_expired: "Session expired"
|
7
|
+
incorrect_user: "Incorrect user"
|
8
|
+
is_not_defined: "%{title} isn't defined"
|
9
|
+
is_not_correct: "%{title} isn't correct"
|
10
|
+
less_than: "%{title} length should be %{size} or more symbols"
|
11
|
+
login_required: "Login required"
|
12
|
+
fields:
|
13
|
+
login: "Login not defined"
|
14
|
+
password: "Password not defined"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
ru:
|
2
|
+
anoubis:
|
3
|
+
errors:
|
4
|
+
incorrect_login: "Некорректный логин или пароль"
|
5
|
+
system_not_defined: "SSO система не определена в Rails.configuration.anoubis_sso_system"
|
6
|
+
session_expired: "Сессия завершена"
|
7
|
+
incorrect_user: "Некорректный пользователь"
|
8
|
+
is_not_defined: "Переменная %{title} не определена"
|
9
|
+
is_not_correct: "Переменная %{title} некорректна"
|
10
|
+
less_than: "Длина переменной %{title} должна быть %{size} или более символов"
|
11
|
+
login_required: "Требуется логин"
|
12
|
+
fields:
|
13
|
+
login: "Логин не задан"
|
14
|
+
password: "Пароль не задан"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,23 @@
|
|
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 'oauth2/auth', to: 'anoubis_sso_server/open_id#auth', as: 'oauth_auth'
|
18
|
+
post 'oauth2/token', to: 'anoubis_sso_server/open_id#access_token', as: 'oauth_token'
|
19
|
+
options 'oauth2/token', to: 'anoubis_sso_server/application#options', as: nil
|
20
|
+
get 'oauth2/logout', to: 'anoubis_sso_server/open_id#logout', as: 'oauth_logout'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
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
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module AnoubisSsoServer
|
2
|
+
##
|
3
|
+
# Main AnoubisSsoServer Engine class
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace AnoubisSsoServer
|
6
|
+
config.generators.api_only = true
|
7
|
+
|
8
|
+
config.generators do |g|
|
9
|
+
g.test_framework :rspec
|
10
|
+
g.fixture_replacement :factory_bot, :dir => 'spec/factories'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/anoubis_sso_server.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require_relative "anoubis_sso_server/version"
|
2
|
+
require_relative "anoubis_sso_server/engine"
|
4
3
|
|
4
|
+
## Main module of library for create basic SSO Server based on OAUTH authentication.
|
5
5
|
module AnoubisSsoServer
|
6
|
-
|
7
|
-
|
6
|
+
## Default error class
|
7
|
+
class Error < StandardError;
|
8
|
+
end
|
8
9
|
end
|
data/sig/anoubis_sso_server.rbs
CHANGED