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