himari 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/himari/access_token.rb +15 -60
- data/lib/himari/app.rb +59 -14
- data/lib/himari/authorization_code.rb +17 -3
- data/lib/himari/client_registration.rb +1 -0
- data/lib/himari/decisions/authorization.rb +14 -4
- data/lib/himari/decisions/claims.rb +6 -2
- data/lib/himari/id_token.rb +2 -1
- data/lib/himari/lifetime_value.rb +15 -0
- 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 +7 -1
- 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,10 +148,11 @@ 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(
|
@@ -138,10 +169,15 @@ module Himari
|
|
138
169
|
).call(env)
|
139
170
|
else
|
140
171
|
logger&.info(Himari::LogLine.new('authorize: prompt login', req: request_as_log, client_id: params[:client_id]))
|
141
|
-
erb
|
172
|
+
erb(config.custom_templates[:login] || :login)
|
142
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
|
+
|
143
179
|
rescue Himari::Services::DownstreamAuthorization::ForbiddenError => e
|
144
|
-
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))
|
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))
|
145
181
|
|
146
182
|
@notice = message_human = e.result.authz_result&.user_facing_message
|
147
183
|
|
@@ -149,8 +185,8 @@ module Himari
|
|
149
185
|
when nil
|
150
186
|
# do nothing
|
151
187
|
when :reauthenticate
|
152
|
-
logger&.warn(Himari::LogLine.new('authorize: prompt login to reauthenticate', req: request_as_log, allowed: e.result.authz_result.allowed, err: e.class.inspect, result: e.as_log))
|
153
|
-
next erb(:login)
|
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)
|
154
190
|
else
|
155
191
|
raise ArgumentError, "Unknown suggestion value for DownstreamAuthorization denial; #{e.as_log.inspect}"
|
156
192
|
end
|
@@ -178,7 +214,8 @@ module Himari
|
|
178
214
|
end
|
179
215
|
get '/oidc/userinfo', &userinfo_ep
|
180
216
|
get '/public/oidc/userinfo', &userinfo_ep
|
181
|
-
|
217
|
+
post '/oidc/userinfo', &userinfo_ep
|
218
|
+
post '/public/oidc/userinfo', &userinfo_ep
|
182
219
|
|
183
220
|
jwks_ep = proc do
|
184
221
|
Himari::Services::JwksEndpoint.new(
|
@@ -201,12 +238,16 @@ module Himari
|
|
201
238
|
|
202
239
|
# do upstream auth
|
203
240
|
authn = Himari::Services::UpstreamAuthentication.from_request(request).perform
|
204
|
-
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))
|
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))
|
205
242
|
raise unless authn.authn_result.allowed # sanity check
|
206
243
|
|
207
244
|
given_back_to = request.env['omniauth.params']&.fetch('back_to', nil)
|
208
245
|
back_to = if given_back_to
|
209
|
-
uri =
|
246
|
+
uri = begin
|
247
|
+
Addressable::URI.parse(given_back_to)
|
248
|
+
rescue Addressable::URI::InvalidURIError
|
249
|
+
nil
|
250
|
+
end
|
210
251
|
if uri && uri.host.nil? && uri.scheme.nil? && uri.path.start_with?('/')
|
211
252
|
given_back_to
|
212
253
|
else
|
@@ -216,10 +257,14 @@ module Himari
|
|
216
257
|
end || '/'
|
217
258
|
|
218
259
|
session.destroy
|
219
|
-
|
260
|
+
|
261
|
+
new_session = authn.session_data
|
262
|
+
config.storage.put_session(new_session)
|
263
|
+
session[:himari_session] = new_session.format.to_s
|
264
|
+
|
220
265
|
redirect back_to
|
221
266
|
rescue Himari::Services::UpstreamAuthentication::UnauthorizedError => e
|
222
|
-
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))
|
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))
|
223
268
|
message_human = e.result.authn_result&.user_facing_message
|
224
269
|
halt(401, "Unauthorized#{message_human ? "; #{message_human}" : nil}")
|
225
270
|
end
|
@@ -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(
|
@@ -23,10 +24,23 @@ module Himari
|
|
23
24
|
)
|
24
25
|
end
|
25
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
|
+
|
26
40
|
alias _expiry_raw expiry
|
27
41
|
private :_expiry_raw
|
28
42
|
def expiry
|
29
|
-
self._expiry_raw || (self.expiry = created_at + (lifetime || 900))
|
43
|
+
self._expiry_raw || (self.expiry = created_at + (lifetime&.code || 900))
|
30
44
|
end
|
31
45
|
|
32
46
|
def valid_redirect_uri?(given_uri)
|
@@ -68,7 +82,7 @@ module Himari
|
|
68
82
|
nonce: nonce,
|
69
83
|
openid: openid,
|
70
84
|
created_at: created_at.to_i,
|
71
|
-
lifetime: lifetime.
|
85
|
+
lifetime: lifetime.as_log,
|
72
86
|
expiry: expiry.to_i,
|
73
87
|
pkce: pkce?,
|
74
88
|
pkce_method: code_challenge_method,
|
@@ -87,7 +101,7 @@ module Himari
|
|
87
101
|
code_challenge: code_challenge,
|
88
102
|
code_challenge_method: code_challenge_method,
|
89
103
|
created_at: created_at.to_i,
|
90
|
-
lifetime: lifetime.
|
104
|
+
lifetime: lifetime.as_json,
|
91
105
|
expiry: expiry.to_i,
|
92
106
|
}
|
93
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
|
@@ -23,22 +24,31 @@ module Himari
|
|
23
24
|
super()
|
24
25
|
@claims = claims
|
25
26
|
@allowed_claims = allowed_claims
|
26
|
-
|
27
|
+
self.lifetime = lifetime
|
27
28
|
end
|
28
29
|
|
29
30
|
attr_reader :claims, :allowed_claims
|
30
|
-
|
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
|
31
41
|
|
32
42
|
def to_evolve_args
|
33
43
|
{
|
34
44
|
claims: @claims.dup,
|
35
45
|
allowed_claims: @allowed_claims.dup,
|
36
|
-
lifetime: @lifetime
|
46
|
+
lifetime: @lifetime,
|
37
47
|
}
|
38
48
|
end
|
39
49
|
|
40
50
|
def as_log
|
41
|
-
to_h.merge(claims: output_claims, lifetime: @lifetime
|
51
|
+
to_h.merge(claims: output_claims, lifetime: @lifetime.to_h)
|
42
52
|
end
|
43
53
|
|
44
54
|
def output_claims
|
@@ -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,11 +7,12 @@ 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,
|
14
|
-
lifetime: authz.lifetime,
|
15
|
+
lifetime: authz.lifetime.is_a?(Integer) ? authz.lifetime : authz.lifetime.id_token, # compat
|
15
16
|
**kwargs
|
16
17
|
)
|
17
18
|
end
|
@@ -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
|
@@ -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;
|
@@ -67,6 +69,10 @@ main > header img, main > footer img {
|
|
67
69
|
margin-bottom: 24px;
|
68
70
|
}
|
69
71
|
|
72
|
+
footer, footer a, footer a:visited {
|
73
|
+
color: #5e5e6b;
|
74
|
+
}
|
75
|
+
|
70
76
|
.d-none {
|
71
77
|
display: none;
|
72
78
|
}
|
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
|