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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ef5f3bac5f8a375751669378420729ab8dbe9ce40c1e1dec0fb5a3ac938b304
4
- data.tar.gz: ed87e0922bc863813624c34d217f02fd9f06fec442fd01a8b0c1daf760b6ca48
3
+ metadata.gz: d532b382f36d5465772fc09e18feebf0a29db47365e2f5fbd3e83cec7eaa6c4b
4
+ data.tar.gz: cf9632d941835355bea9cde2435ad6e9993cb278d306e8d5888fc804053cce32
5
5
  SHA512:
6
- metadata.gz: 97c69604496c88d5b6e0f38cf0693cdd07ee6dd15b73d12e724efcea058a2f44492fcbe57ed5ece878f36e7bc2631a028c1464f9bc17d33f519eb2f17b8ff576
7
- data.tar.gz: 3a9fff6c67bec527df79527f9e436c5703d95e822e0b0d3bc4cc460e003205d238a9e8ea965f2fbb931a18389282dfa2b770e60b931eaadc82dd683b413f841a
6
+ metadata.gz: bb564663bea0c6c39cc0a81d4db848f1e7142b9e2d89ddc34d4b2550d99c44dfc6340dddc239d701ac53d53f762519388095a7806b68d29b675215785cbaf90a
7
+ data.tar.gz: db75ed71b1daef3ae8d3ebbcdf85518c27b9be0fea0a3cc18ca7906271253ac16abc6635c44c5f1b03d3e2a6c3354d02611133e6d71ac4a3e5d6019f756bce47
@@ -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
- class SecretMissing < StandardError; end
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.make(**kwargs)
40
- new(
41
- handler: SecureRandom.urlsafe_base64(32),
42
- secret: SecureRandom.urlsafe_base64(32),
43
- expiry: Time.now.to_i + 3600,
44
- **kwargs
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(handler:, client_id:, claims:, expiry:, secret: nil, secret_hash: nil)
57
- @handler = handler
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 :handler, :client_id, :claims, :expiry
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
- handler_dgst: Digest::SHA256.hexdigest(handler),
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
- handler: handler,
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
- set :protection, use: %i(authenticity_token), except: %i(remote_token)
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
- session[:session_data]
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
- query = Addressable::URI.form_encode(back_to: request.fullpath)
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 config.custom_templates[:login] || :login
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 = Addressable::URI.parse(given_back_to)
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
- session[:session_data] = authn.session_data
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.to_i,
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.to_i,
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
- @lifetime = lifetime
27
+ self.lifetime = lifetime
27
28
  end
28
29
 
29
30
  attr_reader :claims, :allowed_claims
30
- attr_accessor :lifetime
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&.to_i,
46
+ lifetime: @lifetime,
37
47
  }
38
48
  end
39
49
 
40
50
  def as_log
41
- to_h.merge(claims: output_claims, lifetime: @lifetime&.to_i)
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.new(claims: claims, user_data: user_data)
38
+ Himari::SessionData.make(claims: claims, user_data: user_data, lifetime: lifetime)
35
39
  end
36
40
 
37
41
  def initialize_claims!(claims = {})
@@ -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::Format.parse(given_token)
37
+ given_parsed_token = Himari::AccessToken.parse(given_token)
37
38
 
38
- token = @storage.find_token(given_parsed_token.handler)
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::AccessToken::SecretIncorrect, Himari::AccessToken::InvalidFormat, Himari::AccessToken::TokenExpired => e
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
- claims: session_data&.claims,
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,
@@ -1,7 +1,46 @@
1
+ require 'himari/token_string'
2
+
1
3
  module Himari
2
- SessionData = Struct.new(:claims, :user_data, keyword_init: true) do
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
- {claims: claims}
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
@@ -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(handler)
27
- content = read('token', handler)
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.handler, token.as_json, overwrite: overwrite)
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.handler)
38
+ delete_authorization_by_token(token.handle)
37
39
  end
38
40
 
39
- def delete_token_by_handler(handler)
40
- delete('token', handler)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Himari
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -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.3.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-21 00:00:00.000000000 Z
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