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 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