himari 0.3.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 +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
|