himari 0.2.0 → 0.4.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 +4 -4
- data/lib/himari/access_token.rb +15 -60
- data/lib/himari/app.rb +77 -14
- data/lib/himari/authorization_code.rb +27 -1
- data/lib/himari/client_registration.rb +1 -0
- data/lib/himari/decisions/authorization.rb +17 -6
- data/lib/himari/decisions/base.rb +13 -7
- data/lib/himari/decisions/claims.rb +6 -2
- data/lib/himari/id_token.rb +5 -2
- data/lib/himari/lifetime_value.rb +15 -0
- data/lib/himari/rule_processor.rb +7 -2
- data/lib/himari/services/downstream_authorization.rb +5 -4
- data/lib/himari/services/oidc_authorization_endpoint.rb +4 -0
- data/lib/himari/services/oidc_userinfo_endpoint.rb +4 -3
- data/lib/himari/services/upstream_authentication.rb +1 -1
- data/lib/himari/session_data.rb +41 -2
- data/lib/himari/storages/base.rb +24 -6
- data/lib/himari/token_string.rb +96 -0
- data/lib/himari/version.rb +1 -1
- data/public/public/index.css +16 -1
- data/views/login.erb +6 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d532b382f36d5465772fc09e18feebf0a29db47365e2f5fbd3e83cec7eaa6c4b
|
4
|
+
data.tar.gz: cf9632d941835355bea9cde2435ad6e9993cb278d306e8d5888fc804053cce32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb564663bea0c6c39cc0a81d4db848f1e7142b9e2d89ddc34d4b2550d99c44dfc6340dddc239d701ac53d53f762519388095a7806b68d29b675215785cbaf90a
|
7
|
+
data.tar.gz: db75ed71b1daef3ae8d3ebbcdf85518c27b9be0fea0a3cc18ca7906271253ac16abc6635c44c5f1b03d3e2a6c3354d02611133e6d71ac4a3e5d6019f756bce47
|
data/lib/himari/access_token.rb
CHANGED
@@ -1,32 +1,11 @@
|
|
1
|
-
require 'securerandom'
|
2
|
-
require 'base64'
|
3
|
-
require 'digest/sha2'
|
4
|
-
require 'rack/utils'
|
5
|
-
|
6
1
|
require 'rack/oauth2'
|
7
2
|
require 'openid_connect'
|
8
3
|
|
4
|
+
require 'himari/token_string'
|
5
|
+
|
9
6
|
module Himari
|
10
7
|
class AccessToken
|
11
|
-
|
12
|
-
class SecretIncorrect < StandardError; end
|
13
|
-
class TokenExpired < StandardError; end
|
14
|
-
class InvalidFormat < StandardError; end
|
15
|
-
|
16
|
-
Format = Struct.new(:handler, :secret, keyword_init: true) do
|
17
|
-
HEADER = 'hmat'
|
18
|
-
|
19
|
-
def self.parse(str)
|
20
|
-
parts = str.split('.')
|
21
|
-
raise InvalidFormat unless parts.size == 3
|
22
|
-
raise InvalidFormat unless parts[0] == HEADER
|
23
|
-
new(handler: parts[1], secret: parts[2])
|
24
|
-
end
|
25
|
-
|
26
|
-
def to_s
|
27
|
-
"#{HEADER}.#{handler}.#{secret}"
|
28
|
-
end
|
29
|
-
end
|
8
|
+
include TokenString
|
30
9
|
|
31
10
|
class Bearer < Rack::OAuth2::AccessToken::Bearer
|
32
11
|
def token_response(options = {})
|
@@ -36,13 +15,12 @@ module Himari
|
|
36
15
|
end
|
37
16
|
end
|
38
17
|
|
39
|
-
def self.
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
)
|
18
|
+
def self.magic_header
|
19
|
+
'hmat'
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default_lifetime
|
23
|
+
3600
|
46
24
|
end
|
47
25
|
|
48
26
|
# @param authz [Himari::AuthorizationCode]
|
@@ -50,11 +28,12 @@ module Himari
|
|
50
28
|
make(
|
51
29
|
client_id: authz.client_id,
|
52
30
|
claims: authz.claims,
|
31
|
+
lifetime: authz.lifetime.access_token,
|
53
32
|
)
|
54
33
|
end
|
55
34
|
|
56
|
-
def initialize(
|
57
|
-
@
|
35
|
+
def initialize(handle:, client_id:, claims:, expiry:, secret: nil, secret_hash: nil)
|
36
|
+
@handle = handle
|
58
37
|
@client_id = client_id
|
59
38
|
@claims = claims
|
60
39
|
@expiry = expiry
|
@@ -63,32 +42,8 @@ module Himari
|
|
63
42
|
@secret_hash = secret_hash
|
64
43
|
end
|
65
44
|
|
66
|
-
attr_reader :
|
67
|
-
|
68
|
-
def secret
|
69
|
-
raise SecretMissing unless @secret
|
70
|
-
@secret
|
71
|
-
end
|
72
|
-
|
73
|
-
def secret_hash
|
74
|
-
@secret_hash ||= Base64.urlsafe_encode64(Digest::SHA384.digest(secret), padding: false)
|
75
|
-
end
|
76
|
-
|
77
|
-
def verify_secret!(given_secret)
|
78
|
-
dgst = Base64.urlsafe_decode64(secret_hash)
|
79
|
-
given_dgst = Digest::SHA384.digest(given_secret)
|
80
|
-
raise SecretIncorrect unless Rack::Utils.secure_compare(dgst, given_dgst)
|
81
|
-
@secret = given_secret
|
82
|
-
true
|
83
|
-
end
|
45
|
+
attr_reader :handle, :client_id, :claims, :expiry
|
84
46
|
|
85
|
-
def verify_expiry!(now = Time.now)
|
86
|
-
raise TokenExpired if @expiry <= now.to_i
|
87
|
-
end
|
88
|
-
|
89
|
-
def format
|
90
|
-
Format.new(handler: handler, secret: secret)
|
91
|
-
end
|
92
47
|
|
93
48
|
def to_bearer
|
94
49
|
Bearer.new(
|
@@ -99,7 +54,7 @@ module Himari
|
|
99
54
|
|
100
55
|
def as_log
|
101
56
|
{
|
102
|
-
|
57
|
+
handle: handle,
|
103
58
|
client_id: client_id,
|
104
59
|
claims: claims,
|
105
60
|
expiry: expiry,
|
@@ -108,7 +63,7 @@ module Himari
|
|
108
63
|
|
109
64
|
def as_json
|
110
65
|
{
|
111
|
-
|
66
|
+
handle: handle,
|
112
67
|
secret_hash: secret_hash,
|
113
68
|
client_id: client_id,
|
114
69
|
claims: claims,
|
data/lib/himari/app.rb
CHANGED
@@ -6,8 +6,11 @@ require 'himari/version'
|
|
6
6
|
|
7
7
|
require 'himari/log_line'
|
8
8
|
|
9
|
+
require 'himari/token_string'
|
9
10
|
require 'himari/provider_chain'
|
11
|
+
|
10
12
|
require 'himari/authorization_code'
|
13
|
+
require 'himari/session_data'
|
11
14
|
|
12
15
|
require 'himari/middlewares/client'
|
13
16
|
require 'himari/middlewares/config'
|
@@ -26,14 +29,33 @@ module Himari
|
|
26
29
|
class App < Sinatra::Base
|
27
30
|
set :root, File.expand_path(File.join(__dir__, '..', '..'))
|
28
31
|
|
29
|
-
|
32
|
+
# remote_token: disabled in favor of authenticity_token (more stricter)
|
33
|
+
# json_csrf: can be prevented using x-content-type-options:nosniff
|
34
|
+
set :protection, use: %i(authenticity_token), except: %i(remote_token json_csrf)
|
35
|
+
|
30
36
|
set :logging, nil
|
31
37
|
|
32
38
|
ProviderCandidate = Struct.new(:name, :button, :action, keyword_init: true)
|
33
39
|
|
40
|
+
class InvalidSessionToken < StandardError; end
|
41
|
+
|
34
42
|
helpers do
|
35
43
|
def current_user
|
36
|
-
|
44
|
+
return @current_user if defined? @current_user
|
45
|
+
given_token = session[:himari_session]
|
46
|
+
return nil unless given_token
|
47
|
+
|
48
|
+
given_parsed_token = Himari::SessionData.parse(given_token)
|
49
|
+
|
50
|
+
token = config.storage.find_session(given_parsed_token.handle)
|
51
|
+
raise InvalidSessionToken, "no session found in storage (possibly expired)" unless token
|
52
|
+
token.verify!(secret: given_parsed_token.secret)
|
53
|
+
|
54
|
+
@current_user = token
|
55
|
+
rescue InvalidSessionToken, Himari::TokenString::Error => e
|
56
|
+
logger&.warn(Himari::LogLine.new('invalid session token given', req: request_as_log, err: e.class.inspect))
|
57
|
+
session.delete(:himari_session)
|
58
|
+
nil
|
37
59
|
end
|
38
60
|
|
39
61
|
def config
|
@@ -49,7 +71,15 @@ module Himari
|
|
49
71
|
end
|
50
72
|
|
51
73
|
def known_providers
|
52
|
-
|
74
|
+
back_to = if request.query_string.empty?
|
75
|
+
request.path
|
76
|
+
else
|
77
|
+
Addressable::URI.parse(request.fullpath).tap do |u|
|
78
|
+
u.query_values = u.query_values.reject { |k,_v| k == 'prompt' }
|
79
|
+
end.to_s
|
80
|
+
end
|
81
|
+
query = Addressable::URI.form_encode(back_to: back_to)
|
82
|
+
|
53
83
|
config.providers.map do |pr|
|
54
84
|
name = pr.fetch(:name)
|
55
85
|
ProviderCandidate.new(
|
@@ -109,7 +139,7 @@ module Himari
|
|
109
139
|
|
110
140
|
get '/' do
|
111
141
|
content_type :text
|
112
|
-
"Himari\n"
|
142
|
+
"Himari #{release_code}\n"
|
113
143
|
end
|
114
144
|
|
115
145
|
get '/oidc/authorize' do
|
@@ -118,15 +148,17 @@ module Himari
|
|
118
148
|
logger&.warn(Himari::LogLine.new('authorize: no client registration found', req: request_as_log, client_id: params[:client_id]))
|
119
149
|
next halt 401, 'unknown client'
|
120
150
|
end
|
151
|
+
|
121
152
|
if current_user
|
122
153
|
# do downstream authz and process oidc request
|
123
154
|
decision = Himari::Services::DownstreamAuthorization.from_request(session: current_user, client: client, request: request).perform
|
124
|
-
logger&.info(Himari::LogLine.new('authorize: downstream authorized', req: request_as_log, allowed: decision.authz_result.allowed, result: decision.as_log))
|
155
|
+
logger&.info(Himari::LogLine.new('authorize: downstream authorized', req: request_as_log, session: current_user.as_log, allowed: decision.authz_result.allowed, result: decision.as_log))
|
125
156
|
raise unless decision.authz_result.allowed # sanity check
|
126
157
|
|
127
158
|
authz = AuthorizationCode.make(
|
128
159
|
client_id: decision.client.id,
|
129
160
|
claims: decision.claims,
|
161
|
+
lifetime: decision.lifetime,
|
130
162
|
)
|
131
163
|
|
132
164
|
Himari::Services::OidcAuthorizationEndpoint.new(
|
@@ -137,11 +169,29 @@ module Himari
|
|
137
169
|
).call(env)
|
138
170
|
else
|
139
171
|
logger&.info(Himari::LogLine.new('authorize: prompt login', req: request_as_log, client_id: params[:client_id]))
|
140
|
-
erb
|
172
|
+
erb(config.custom_templates[:login] || :login)
|
141
173
|
end
|
174
|
+
|
175
|
+
rescue Himari::Services::OidcAuthorizationEndpoint::ReauthenticationRequired
|
176
|
+
logger&.warn(Himari::LogLine.new('authorize: prompt login to reauthenticate (demanded by oidc request)', req: request_as_log, session: current_user&.as_log, allowed: decision&.authz_result&.allowed, result: decision&.as_log))
|
177
|
+
next erb(config.custom_templates[:login] || :login)
|
178
|
+
|
142
179
|
rescue Himari::Services::DownstreamAuthorization::ForbiddenError => e
|
143
|
-
logger&.warn(Himari::LogLine.new('authorize: downstream forbidden', req: request_as_log, allowed: e.result.authz_result.allowed, err: e.class.inspect, result: e.as_log))
|
144
|
-
|
180
|
+
logger&.warn(Himari::LogLine.new('authorize: downstream forbidden', req: request_as_log, session: current_user&.as_log, allowed: e.result.authz_result.allowed, err: e.class.inspect, result: e.as_log))
|
181
|
+
|
182
|
+
@notice = message_human = e.result.authz_result&.user_facing_message
|
183
|
+
|
184
|
+
case e.result.authz_result&.suggestion
|
185
|
+
when nil
|
186
|
+
# do nothing
|
187
|
+
when :reauthenticate
|
188
|
+
logger&.warn(Himari::LogLine.new('authorize: prompt login to reauthenticate (suggested by decision)', req: request_as_log, session: current_user&.as_log, allowed: e.result.authz_result.allowed, err: e.class.inspect, result: e.as_log))
|
189
|
+
next erb(config.custom_templates[:login] || :login)
|
190
|
+
else
|
191
|
+
raise ArgumentError, "Unknown suggestion value for DownstreamAuthorization denial; #{e.as_log.inspect}"
|
192
|
+
end
|
193
|
+
|
194
|
+
halt(403, "Forbidden#{message_human ? "; #{message_human}" : nil}")
|
145
195
|
end
|
146
196
|
|
147
197
|
token_ep = proc do
|
@@ -164,7 +214,8 @@ module Himari
|
|
164
214
|
end
|
165
215
|
get '/oidc/userinfo', &userinfo_ep
|
166
216
|
get '/public/oidc/userinfo', &userinfo_ep
|
167
|
-
|
217
|
+
post '/oidc/userinfo', &userinfo_ep
|
218
|
+
post '/public/oidc/userinfo', &userinfo_ep
|
168
219
|
|
169
220
|
jwks_ep = proc do
|
170
221
|
Himari::Services::JwksEndpoint.new(
|
@@ -182,14 +233,21 @@ module Himari
|
|
182
233
|
end
|
183
234
|
|
184
235
|
omniauth_callback = proc do
|
236
|
+
authhash = request.env['omniauth.auth']
|
237
|
+
next halt(400, 'Bad Request') unless authhash
|
238
|
+
|
185
239
|
# do upstream auth
|
186
240
|
authn = Himari::Services::UpstreamAuthentication.from_request(request).perform
|
187
|
-
logger&.info(Himari::LogLine.new('authentication allowed', req: request_as_log, allowed: authn.authn_result.allowed, uid:
|
241
|
+
logger&.info(Himari::LogLine.new('authentication allowed', req: request_as_log, allowed: authn.authn_result.allowed, uid: authhash[:uid], provider: authhash[:provider], result: authn.as_log, existing_session: current_user&.as_log))
|
188
242
|
raise unless authn.authn_result.allowed # sanity check
|
189
243
|
|
190
244
|
given_back_to = request.env['omniauth.params']&.fetch('back_to', nil)
|
191
245
|
back_to = if given_back_to
|
192
|
-
uri =
|
246
|
+
uri = begin
|
247
|
+
Addressable::URI.parse(given_back_to)
|
248
|
+
rescue Addressable::URI::InvalidURIError
|
249
|
+
nil
|
250
|
+
end
|
193
251
|
if uri && uri.host.nil? && uri.scheme.nil? && uri.path.start_with?('/')
|
194
252
|
given_back_to
|
195
253
|
else
|
@@ -199,11 +257,16 @@ module Himari
|
|
199
257
|
end || '/'
|
200
258
|
|
201
259
|
session.destroy
|
202
|
-
|
260
|
+
|
261
|
+
new_session = authn.session_data
|
262
|
+
config.storage.put_session(new_session)
|
263
|
+
session[:himari_session] = new_session.format.to_s
|
264
|
+
|
203
265
|
redirect back_to
|
204
266
|
rescue Himari::Services::UpstreamAuthentication::UnauthorizedError => e
|
205
|
-
logger&.warn(Himari::LogLine.new('authentication denied', req: request_as_log, err: e.class.inspect, allowed: e.result.authn_result.allowed, uid: request.env.fetch('omniauth.auth')[:uid], provider: request.env.fetch('omniauth.auth')[:provider], result: e.as_log))
|
206
|
-
|
267
|
+
logger&.warn(Himari::LogLine.new('authentication denied', req: request_as_log, err: e.class.inspect, allowed: e.result.authn_result.allowed, uid: request.env.fetch('omniauth.auth')[:uid], provider: request.env.fetch('omniauth.auth')[:provider], result: e.as_log, existing_session: current_user&.as_log))
|
268
|
+
message_human = e.result.authn_result&.user_facing_message
|
269
|
+
halt(401, "Unauthorized#{message_human ? "; #{message_human}" : nil}")
|
207
270
|
end
|
208
271
|
get '/auth/:provider/callback', &omniauth_callback
|
209
272
|
post '/auth/:provider/callback', &omniauth_callback
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'digest/sha2'
|
2
|
+
require 'himari/lifetime_value'
|
2
3
|
|
3
4
|
module Himari
|
4
5
|
authz_attrs = %i(
|
@@ -10,17 +11,38 @@ module Himari
|
|
10
11
|
nonce
|
11
12
|
code_challenge
|
12
13
|
code_challenge_method
|
14
|
+
created_at
|
15
|
+
lifetime
|
13
16
|
expiry
|
14
17
|
)
|
15
18
|
AuthorizationCode = Struct.new(*authz_attrs, keyword_init: true) do
|
16
19
|
def self.make(**kwargs)
|
17
20
|
new(
|
18
21
|
code: SecureRandom.urlsafe_base64(32),
|
19
|
-
|
22
|
+
created_at: Time.now.to_i,
|
20
23
|
**kwargs,
|
21
24
|
)
|
22
25
|
end
|
23
26
|
|
27
|
+
alias _lifetime_raw lifetime
|
28
|
+
private :_lifetime_raw
|
29
|
+
def lifetime
|
30
|
+
case _lifetime_raw
|
31
|
+
when Hash
|
32
|
+
self.lifetime = LifetimeValue.new(**_lifetime_raw)
|
33
|
+
when Integer #compat
|
34
|
+
self.lifetime = LifetimeValue.from_integer(_lifetime_raw)
|
35
|
+
else
|
36
|
+
_lifetime_raw
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
alias _expiry_raw expiry
|
41
|
+
private :_expiry_raw
|
42
|
+
def expiry
|
43
|
+
self._expiry_raw || (self.expiry = created_at + (lifetime&.code || 900))
|
44
|
+
end
|
45
|
+
|
24
46
|
def valid_redirect_uri?(given_uri)
|
25
47
|
redirect_uri == given_uri
|
26
48
|
end
|
@@ -59,6 +81,8 @@ module Himari
|
|
59
81
|
claims: claims,
|
60
82
|
nonce: nonce,
|
61
83
|
openid: openid,
|
84
|
+
created_at: created_at.to_i,
|
85
|
+
lifetime: lifetime.as_log,
|
62
86
|
expiry: expiry.to_i,
|
63
87
|
pkce: pkce?,
|
64
88
|
pkce_method: code_challenge_method,
|
@@ -76,6 +100,8 @@ module Himari
|
|
76
100
|
nonce: nonce,
|
77
101
|
code_challenge: code_challenge,
|
78
102
|
code_challenge_method: code_challenge_method,
|
103
|
+
created_at: created_at.to_i,
|
104
|
+
lifetime: lifetime.as_json,
|
79
105
|
expiry: expiry.to_i,
|
80
106
|
}
|
81
107
|
end
|
@@ -10,6 +10,7 @@ module Himari
|
|
10
10
|
@redirect_uris = redirect_uris
|
11
11
|
@preferred_key_group = preferred_key_group
|
12
12
|
|
13
|
+
raise ArgumentError, "name starts with '_' is reserved" if @name&.start_with?('_')
|
13
14
|
raise ArgumentError, "either secret or secret_hash must be present" if !@secret && !@secret_hash
|
14
15
|
end
|
15
16
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'himari/decisions/base'
|
2
|
+
require 'himari/lifetime_value'
|
2
3
|
|
3
4
|
module Himari
|
4
5
|
module Decisions
|
@@ -19,28 +20,38 @@ module Himari
|
|
19
20
|
|
20
21
|
allow_effects(:allow, :deny, :continue, :skip)
|
21
22
|
|
22
|
-
def initialize(claims: {}, allowed_claims: DEFAULT_ALLOWED_CLAIMS, lifetime: 3600
|
23
|
+
def initialize(claims: {}, allowed_claims: DEFAULT_ALLOWED_CLAIMS, lifetime: 3600)
|
23
24
|
super()
|
24
25
|
@claims = claims
|
25
26
|
@allowed_claims = allowed_claims
|
26
|
-
|
27
|
+
self.lifetime = lifetime
|
27
28
|
end
|
28
29
|
|
29
|
-
attr_reader :claims, :allowed_claims
|
30
|
+
attr_reader :claims, :allowed_claims
|
31
|
+
attr_reader :lifetime
|
32
|
+
|
33
|
+
def lifetime=(x)
|
34
|
+
case x
|
35
|
+
when LifetimeValue
|
36
|
+
@lifetime = x
|
37
|
+
else
|
38
|
+
@lifetime = LifetimeValue.from_integer(x)
|
39
|
+
end
|
40
|
+
end
|
30
41
|
|
31
42
|
def to_evolve_args
|
32
43
|
{
|
33
44
|
claims: @claims.dup,
|
34
45
|
allowed_claims: @allowed_claims.dup,
|
35
|
-
lifetime: @lifetime
|
46
|
+
lifetime: @lifetime,
|
36
47
|
}
|
37
48
|
end
|
38
49
|
|
39
50
|
def as_log
|
40
|
-
to_h.merge(claims:
|
51
|
+
to_h.merge(claims: output_claims, lifetime: @lifetime.to_h)
|
41
52
|
end
|
42
53
|
|
43
|
-
def
|
54
|
+
def output_claims
|
44
55
|
claims.select { |k,_v| allowed_claims.include?(k) }
|
45
56
|
end
|
46
57
|
end
|
@@ -18,7 +18,7 @@ module Himari
|
|
18
18
|
raise "#{self.class.name}.valid_effects is missing [BUG]" unless self.class.valid_effects
|
19
19
|
end
|
20
20
|
|
21
|
-
attr_reader :effect, :effect_comment, :rule_name
|
21
|
+
attr_reader :effect, :effect_comment, :effect_user_facing_message, :effect_suggestion, :rule_name
|
22
22
|
|
23
23
|
def to_evolve_args
|
24
24
|
raise NotImplementedError
|
@@ -29,7 +29,10 @@ module Himari
|
|
29
29
|
rule_name: rule_name,
|
30
30
|
effect: effect,
|
31
31
|
effect_comment: effect_comment,
|
32
|
-
}
|
32
|
+
}.tap do |x|
|
33
|
+
x[:effect_user_facing_message] = effect_user_facing_message if effect_user_facing_message
|
34
|
+
x[:effect_suggestion] = effect_suggestion if effect_suggestion
|
35
|
+
end
|
33
36
|
end
|
34
37
|
|
35
38
|
def as_log
|
@@ -46,18 +49,21 @@ module Himari
|
|
46
49
|
self
|
47
50
|
end
|
48
51
|
|
49
|
-
def decide!(effect, comment = "")
|
52
|
+
def decide!(effect, comment = "", user_facing_message: nil, suggest: nil)
|
50
53
|
raise DecisionAlreadyMade, "decision can only be made once per rule (#{rule_name})" if @effect
|
51
54
|
raise InvalidEffect, "this effect is not valid under this rule. Valid effects: #{self.class.valid_effects.inspect} (#{rule_name})" unless self.class.valid_effects.include?(effect)
|
55
|
+
raise InvalidEffect, "only deny effect can have suggestion" if suggest&& effect != :deny
|
52
56
|
@effect = effect
|
53
57
|
@effect_comment = comment
|
58
|
+
@effect_user_facing_message = user_facing_message
|
59
|
+
@effect_suggestion = suggest
|
54
60
|
nil
|
55
61
|
end
|
56
62
|
|
57
|
-
def allow!(
|
58
|
-
def continue!(
|
59
|
-
def deny!(
|
60
|
-
def skip!(
|
63
|
+
def allow!(*args, **kwargs); decide!(:allow, *args, **kwargs); end
|
64
|
+
def continue!(*args, **kwargs); decide!(:continue, *args, **kwargs); end
|
65
|
+
def deny!(*args, **kwargs); decide!(:deny, *args, **kwargs); end
|
66
|
+
def skip!(*args, **kwargs); decide!(:skip, *args, **kwargs); end
|
61
67
|
end
|
62
68
|
end
|
63
69
|
end
|
@@ -13,16 +13,20 @@ module Himari
|
|
13
13
|
|
14
14
|
allow_effects(:continue, :skip)
|
15
15
|
|
16
|
-
def initialize(claims: nil, user_data: nil)
|
16
|
+
def initialize(claims: nil, user_data: nil, lifetime: nil)
|
17
17
|
super()
|
18
18
|
@claims = claims
|
19
19
|
@user_data = user_data
|
20
|
+
@lifetime = lifetime
|
20
21
|
end
|
21
22
|
|
23
|
+
attr_accessor :lifetime
|
24
|
+
|
22
25
|
def to_evolve_args
|
23
26
|
{
|
24
27
|
claims: @claims.dup,
|
25
28
|
user_data: @user_data.dup,
|
29
|
+
lifetime: @lifetime&.to_i,
|
26
30
|
}
|
27
31
|
end
|
28
32
|
|
@@ -31,7 +35,7 @@ module Himari
|
|
31
35
|
end
|
32
36
|
|
33
37
|
def output
|
34
|
-
Himari::SessionData.
|
38
|
+
Himari::SessionData.make(claims: claims, user_data: user_data, lifetime: lifetime)
|
35
39
|
end
|
36
40
|
|
37
41
|
def initialize_claims!(claims = {})
|
data/lib/himari/id_token.rb
CHANGED
@@ -7,15 +7,17 @@ module Himari
|
|
7
7
|
class IdToken
|
8
8
|
# @param authz [Himari::AuthorizationCode]
|
9
9
|
def self.from_authz(authz, **kwargs)
|
10
|
+
|
10
11
|
new(
|
11
12
|
claims: authz.claims,
|
12
13
|
client_id: authz.client_id,
|
13
14
|
nonce: authz.nonce,
|
15
|
+
lifetime: authz.lifetime.is_a?(Integer) ? authz.lifetime : authz.lifetime.id_token, # compat
|
14
16
|
**kwargs
|
15
17
|
)
|
16
18
|
end
|
17
19
|
|
18
|
-
def initialize(claims:, client_id:, nonce:, signing_key:, issuer:, access_token: nil, time: Time.now)
|
20
|
+
def initialize(claims:, client_id:, nonce:, signing_key:, issuer:, access_token: nil, time: Time.now, lifetime: 3600)
|
19
21
|
@claims = claims
|
20
22
|
@client_id = client_id
|
21
23
|
@nonce = nonce
|
@@ -23,6 +25,7 @@ module Himari
|
|
23
25
|
@issuer = issuer
|
24
26
|
@access_token = access_token
|
25
27
|
@time = time
|
28
|
+
@lifetime = lifetime
|
26
29
|
end
|
27
30
|
|
28
31
|
attr_reader :claims, :nonce, :signing_key
|
@@ -34,7 +37,7 @@ module Himari
|
|
34
37
|
aud: @client_id,
|
35
38
|
iat: @time.to_i,
|
36
39
|
nbf: @time.to_i,
|
37
|
-
exp: (@time +
|
40
|
+
exp: (@time + @lifetime).to_i,
|
38
41
|
).merge(
|
39
42
|
@nonce ? { nonce: @nonce } : {}
|
40
43
|
).merge(
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Himari
|
2
|
+
LifetimeValue = Struct.new(:access_token, :id_token, :code, keyword_init: true) do
|
3
|
+
def self.from_integer(i)
|
4
|
+
new(access_token: i, id_token: i, code: nil)
|
5
|
+
end
|
6
|
+
|
7
|
+
def as_log
|
8
|
+
as_json&.compact
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
{access_token: access_token, id_token: id_token, code: code}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -2,7 +2,7 @@ module Himari
|
|
2
2
|
class RuleProcessor
|
3
3
|
class MissingDecisionError < StandardError; end
|
4
4
|
|
5
|
-
Result = Struct.new(:rule_name, :allowed, :explicit_deny, :decision, :decision_log, keyword_init: true) do
|
5
|
+
Result = Struct.new(:rule_name, :allowed, :explicit_deny, :decision, :decision_log, :user_facing_message, :suggestion, keyword_init: true) do
|
6
6
|
def as_log
|
7
7
|
{
|
8
8
|
rule_name: rule_name,
|
@@ -10,7 +10,9 @@ module Himari
|
|
10
10
|
explicit_deny: explicit_deny,
|
11
11
|
decision: decision&.as_log,
|
12
12
|
decision_log: decision_log.map(&:to_h),
|
13
|
-
}
|
13
|
+
}.tap do |x|
|
14
|
+
x[:suggestion] = suggestion if suggestion
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
@@ -47,6 +49,7 @@ module Himari
|
|
47
49
|
result.decision = decision
|
48
50
|
result.allowed = true
|
49
51
|
result.explicit_deny = false
|
52
|
+
result.user_facing_message = decision.effect_user_facing_message
|
50
53
|
|
51
54
|
when :continue
|
52
55
|
@decision = decision
|
@@ -61,6 +64,8 @@ module Himari
|
|
61
64
|
result.decision = nil
|
62
65
|
result.allowed = false
|
63
66
|
result.explicit_deny = true
|
67
|
+
result.user_facing_message = decision.effect_user_facing_message
|
68
|
+
result.suggestion = decision.effect_suggestion
|
64
69
|
|
65
70
|
else
|
66
71
|
raise "Unknown effect #{decision.effect} [BUG]"
|
@@ -21,7 +21,7 @@ module Himari
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
Result = Struct.new(:client, :claims, :authz_result) do
|
24
|
+
Result = Struct.new(:client, :claims, :lifetime, :authz_result) do
|
25
25
|
def as_log
|
26
26
|
{
|
27
27
|
client: client.as_log,
|
@@ -63,10 +63,11 @@ module Himari
|
|
63
63
|
context = Himari::Decisions::Authorization::Context.new(claims: @session.claims, user_data: @session.user_data, request: @request, client: @client).freeze
|
64
64
|
|
65
65
|
authorization = Himari::RuleProcessor.new(context, Himari::Decisions::Authorization.new(claims: @session.claims.dup)).run(@authz_rules)
|
66
|
-
raise ForbiddenError.new(Result.new(@client, nil, authorization)) unless authorization.allowed
|
66
|
+
raise ForbiddenError.new(Result.new(@client, nil, nil, authorization)) unless authorization.allowed
|
67
67
|
|
68
|
-
claims = authorization.decision.
|
69
|
-
|
68
|
+
claims = authorization.decision.output_claims
|
69
|
+
lifetime = authorization.decision.lifetime
|
70
|
+
Result.new(@client, claims, lifetime, authorization)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
end
|
@@ -5,6 +5,8 @@ require 'openid_connect'
|
|
5
5
|
module Himari
|
6
6
|
module Services
|
7
7
|
class OidcAuthorizationEndpoint
|
8
|
+
class ReauthenticationRequired < StandardError; end
|
9
|
+
|
8
10
|
SUPPORTED_RESPONSE_TYPES = ['code'] # TODO: share with oidc metadata
|
9
11
|
|
10
12
|
# @param authz [Himari::AuthorizationCode] pending (unpersisted) authz data
|
@@ -39,6 +41,8 @@ module Himari
|
|
39
41
|
|
40
42
|
req.unsupported_response_type! if res.protocol_params_location == :fragment
|
41
43
|
req.bad_request!(:request_uri_not_supported, "Request Object is not implemented") if req.request_uri || req.request
|
44
|
+
req.bad_request!(:invalid_request, 'prompt=none should not contain any other value') if req.prompt.include?('none') && req.prompt.any? { |x| x != 'none' }
|
45
|
+
raise ReauthenticationRequired if req.prompt.include?('login') || req.prompt.include?('select_account')
|
42
46
|
|
43
47
|
requested_response_types = [*req.response_type]
|
44
48
|
unless SUPPORTED_RESPONSE_TYPES.include?(requested_response_types.map(&:to_s).join(' '))
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'himari/access_token'
|
2
|
+
require 'himari/token_string'
|
2
3
|
require 'himari/log_line'
|
3
4
|
|
4
5
|
module Himari
|
@@ -33,9 +34,9 @@ module Himari
|
|
33
34
|
return [404, {'Content-Type' => 'application/json'}, ['{"error": "not_found"}']] unless %w(GET POST).include?(@env['REQUEST_METHOD'])
|
34
35
|
|
35
36
|
raise InvalidToken unless given_token
|
36
|
-
given_parsed_token = Himari::AccessToken
|
37
|
+
given_parsed_token = Himari::AccessToken.parse(given_token)
|
37
38
|
|
38
|
-
token = @storage.find_token(given_parsed_token.
|
39
|
+
token = @storage.find_token(given_parsed_token.handle)
|
39
40
|
raise InvalidToken unless token
|
40
41
|
token.verify_expiry!()
|
41
42
|
token.verify_secret!(given_parsed_token.secret)
|
@@ -46,7 +47,7 @@ module Himari
|
|
46
47
|
{'Content-Type' => 'application/json; charset=utf-8'},
|
47
48
|
[JSON.pretty_generate(token.claims), "\n"],
|
48
49
|
]
|
49
|
-
rescue InvalidToken, Himari::
|
50
|
+
rescue InvalidToken, Himari::TokenString::SecretIncorrect, Himari::TokenString::InvalidFormat, Himari::TokenString::TokenExpired => e
|
50
51
|
@logger&.warn(Himari::LogLine.new('OidcUserinfoEndpoint: invalid_token', req: @env['himari.request_as_log'], err: e.class.inspect, token: token&.as_log))
|
51
52
|
[
|
52
53
|
401,
|
@@ -27,7 +27,7 @@ module Himari
|
|
27
27
|
Result = Struct.new(:claims_result, :authn_result, :session_data) do
|
28
28
|
def as_log
|
29
29
|
{
|
30
|
-
|
30
|
+
session: session_data&.as_log,
|
31
31
|
decision: {
|
32
32
|
claims: claims_result&.as_log&.reject{ |k,_v| %i(allowed explicit_deny).include?(k) },
|
33
33
|
authentication: authn_result&.as_log,
|
data/lib/himari/session_data.rb
CHANGED
@@ -1,7 +1,46 @@
|
|
1
|
+
require 'himari/token_string'
|
2
|
+
|
1
3
|
module Himari
|
2
|
-
SessionData
|
4
|
+
class SessionData
|
5
|
+
include Himari::TokenString
|
6
|
+
|
7
|
+
def initialize(claims: {}, user_data: {}, handle:, secret: nil, secret_hash: nil, expiry: nil)
|
8
|
+
@claims = claims
|
9
|
+
@user_data = user_data
|
10
|
+
|
11
|
+
@handle = handle
|
12
|
+
@secret = secret
|
13
|
+
@secret_hash = secret_hash
|
14
|
+
@expiry = expiry
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.magic_header
|
18
|
+
'hmas'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.default_lifetime
|
22
|
+
3600
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :claims, :user_data
|
26
|
+
|
3
27
|
def as_log
|
4
|
-
{
|
28
|
+
{
|
29
|
+
handle: handle,
|
30
|
+
claims: claims,
|
31
|
+
expiry: expiry,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def as_json
|
36
|
+
{
|
37
|
+
handle: handle,
|
38
|
+
secret_hash: secret_hash,
|
39
|
+
expiry: expiry,
|
40
|
+
|
41
|
+
claims: claims,
|
42
|
+
user_data: user_data,
|
43
|
+
}
|
5
44
|
end
|
6
45
|
end
|
7
46
|
end
|
data/lib/himari/storages/base.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'himari/authorization_code'
|
2
2
|
require 'himari/access_token'
|
3
|
+
require 'himari/session_data'
|
3
4
|
|
4
5
|
module Himari
|
5
6
|
module Storages
|
@@ -23,23 +24,40 @@ module Himari
|
|
23
24
|
delete('authz', code)
|
24
25
|
end
|
25
26
|
|
26
|
-
def find_token(
|
27
|
-
content = read('token',
|
27
|
+
def find_token(handle)
|
28
|
+
content = read('token', handle)
|
29
|
+
content[:handle] = content.delete(:handle) if content.key?(:handler) # compat
|
28
30
|
content && AccessToken.new(**content)
|
29
31
|
end
|
30
32
|
|
31
33
|
def put_token(token, overwrite: false)
|
32
|
-
write('token', token.
|
34
|
+
write('token', token.handle, token.as_json, overwrite: overwrite)
|
33
35
|
end
|
34
36
|
|
35
37
|
def delete_token(token)
|
36
|
-
delete_authorization_by_token(token.
|
38
|
+
delete_authorization_by_token(token.handle)
|
37
39
|
end
|
38
40
|
|
39
|
-
def
|
40
|
-
delete('token',
|
41
|
+
def delete_token_by_handle(handle)
|
42
|
+
delete('token', handle)
|
41
43
|
end
|
42
44
|
|
45
|
+
def find_session(handle)
|
46
|
+
content = read('session', handle)
|
47
|
+
content && SessionData.new(**content)
|
48
|
+
end
|
49
|
+
|
50
|
+
def put_session(session, overwrite: false)
|
51
|
+
write('session', session.handle, session.as_json, overwrite: overwrite)
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_session(session)
|
55
|
+
delete_session_by_handle(session.handle)
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete_session_by_handle(handle)
|
59
|
+
delete('session', handle)
|
60
|
+
end
|
43
61
|
|
44
62
|
private def write(kind, key, content, overwrite: false)
|
45
63
|
raise NotImplementedError
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'base64'
|
3
|
+
require 'digest/sha2'
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
module Himari
|
7
|
+
module TokenString
|
8
|
+
class Error < StandardError; end
|
9
|
+
class SecretMissing < Error; end
|
10
|
+
class SecretIncorrect < Error; end
|
11
|
+
class TokenExpired < Error; end
|
12
|
+
class InvalidFormat < Error; end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def magic_header
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_lifetime
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def make(lifetime: nil, **kwargs)
|
24
|
+
new(
|
25
|
+
handle: SecureRandom.urlsafe_base64(32),
|
26
|
+
secret: SecureRandom.urlsafe_base64(48),
|
27
|
+
expiry: Time.now.to_i + (lifetime || default_lifetime),
|
28
|
+
**kwargs
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(str)
|
33
|
+
Format.parse(magic_header, str)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.included(k)
|
38
|
+
k.extend(ClassMethods)
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle
|
42
|
+
@handle
|
43
|
+
end
|
44
|
+
|
45
|
+
def expiry
|
46
|
+
@expiry
|
47
|
+
end
|
48
|
+
|
49
|
+
def secret
|
50
|
+
raise SecretMissing unless @secret
|
51
|
+
@secret
|
52
|
+
end
|
53
|
+
|
54
|
+
def secret_hash
|
55
|
+
@secret_hash ||= Base64.urlsafe_encode64(Digest::SHA384.digest(secret), padding: false)
|
56
|
+
end
|
57
|
+
|
58
|
+
def verify!(secret:, now: Time.now)
|
59
|
+
verify_expiry!(now)
|
60
|
+
verify_secret!(secret)
|
61
|
+
end
|
62
|
+
|
63
|
+
def verify_secret!(given_secret)
|
64
|
+
dgst = Base64.urlsafe_decode64(secret_hash) # TODO: rescue errors
|
65
|
+
given_dgst = Digest::SHA384.digest(given_secret)
|
66
|
+
raise SecretIncorrect unless Rack::Utils.secure_compare(dgst, given_dgst)
|
67
|
+
@secret = given_secret
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def verify_expiry!(now = Time.now)
|
72
|
+
raise TokenExpired if @expiry <= now.to_i
|
73
|
+
end
|
74
|
+
|
75
|
+
Format = Struct.new(:header, :handle, :secret, keyword_init: true) do
|
76
|
+
def self.parse(header, str)
|
77
|
+
parts = str.split('.')
|
78
|
+
raise InvalidFormat unless parts.size == 3
|
79
|
+
raise InvalidFormat unless parts[0] == header
|
80
|
+
new(header: header, handle: parts[1], secret: parts[2])
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
"#{header}.#{handle}.#{secret}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def magic_header
|
89
|
+
self.class.magic_header
|
90
|
+
end
|
91
|
+
|
92
|
+
def format
|
93
|
+
Format.new(header: magic_header, handle: handle, secret: secret)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/himari/version.rb
CHANGED
data/public/public/index.css
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
body {
|
1
|
+
body, button, select {
|
2
2
|
font-family: "Segoe UI", Helvetica, sans-serif;
|
3
|
+
}
|
4
|
+
body {
|
3
5
|
font-size: 16px;
|
4
6
|
background-color: #FDF7EF;
|
5
7
|
text-align: center;
|
@@ -58,6 +60,19 @@ main > header img, main > footer img {
|
|
58
60
|
margin-top: 30px;
|
59
61
|
}
|
60
62
|
|
63
|
+
.notice {
|
64
|
+
background-color: white;
|
65
|
+
border: 1px #bfa88a solid;
|
66
|
+
border-radius: 4px;
|
67
|
+
padding: 4px;
|
68
|
+
margin: 12px;
|
69
|
+
margin-bottom: 24px;
|
70
|
+
}
|
71
|
+
|
72
|
+
footer, footer a, footer a:visited {
|
73
|
+
color: #5e5e6b;
|
74
|
+
}
|
75
|
+
|
61
76
|
.d-none {
|
62
77
|
display: none;
|
63
78
|
}
|
data/views/login.erb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: himari
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sorah Fukumori
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- lib/himari/id_token.rb
|
118
118
|
- lib/himari/item_provider.rb
|
119
119
|
- lib/himari/item_providers/static.rb
|
120
|
+
- lib/himari/lifetime_value.rb
|
120
121
|
- lib/himari/log_line.rb
|
121
122
|
- lib/himari/middlewares/authentication_rule.rb
|
122
123
|
- lib/himari/middlewares/authorization_rule.rb
|
@@ -139,6 +140,7 @@ files:
|
|
139
140
|
- lib/himari/storages/base.rb
|
140
141
|
- lib/himari/storages/filesystem.rb
|
141
142
|
- lib/himari/storages/memory.rb
|
143
|
+
- lib/himari/token_string.rb
|
142
144
|
- lib/himari/version.rb
|
143
145
|
- public/public/index.css
|
144
146
|
- sig/himari.rbs
|