# frozen-string-literal: true module Rodauth Feature.define(:jwt_refresh, :JwtRefresh) do depends :jwt after 'refresh_token' before 'refresh_token' auth_value_method :allow_refresh_with_expired_jwt_access_token?, false session_key :jwt_refresh_token_data_session_key, :jwt_refresh_token_data session_key :jwt_refresh_token_hmac_session_key, :jwt_refresh_token_hash auth_value_method :jwt_access_token_key, 'access_token' auth_value_method :jwt_access_token_not_before_period, 5 auth_value_method :jwt_access_token_period, 1800 translatable_method :jwt_refresh_invalid_token_message, 'invalid JWT refresh token' auth_value_method :jwt_refresh_token_account_id_column, :account_id auth_value_method :jwt_refresh_token_deadline_column, :deadline auth_value_method :jwt_refresh_token_deadline_interval, {:days=>14}.freeze auth_value_method :jwt_refresh_token_id_column, :id auth_value_method :jwt_refresh_token_key, 'refresh_token' auth_value_method :jwt_refresh_token_key_column, :key auth_value_method :jwt_refresh_token_key_param, 'refresh_token' auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh' auth_value_method :jwt_refresh_without_access_token_status, 401 translatable_method :expired_jwt_access_token_message, "expired JWT access token" auth_value_method :expired_jwt_access_token_status, 400 auth_private_methods( :account_from_refresh_token ) route do |r| # For backward compatibility, unused in core Rodauth # RODAUTH3: Remove @jwt_refresh_route = true before_jwt_refresh_route r.post do if !session_value response.status ||= jwt_refresh_without_access_token_status json_response[json_response_error_key] = jwt_refresh_without_access_token_message elsif (refresh_token = param_or_nil(jwt_refresh_token_key_param)) && account_from_refresh_token(refresh_token) transaction do before_refresh_token formatted_token = generate_refresh_token remove_jwt_refresh_token_key(refresh_token) set_jwt_refresh_token_hmac_session_key(formatted_token) json_response[jwt_refresh_token_key] = formatted_token json_response[jwt_access_token_key] = session_jwt after_refresh_token end else json_response[json_response_error_key] = jwt_refresh_invalid_token_message end _return_json_response end end def update_session super # JWT login puts the access token in the header. # We put the refresh token in the body. # Note, do not put the access_token in the body here, as the access token content is not yet finalised. token = json_response[jwt_refresh_token_key] = generate_refresh_token set_jwt_refresh_token_hmac_session_key(token) end def set_jwt_token(token) super if json_response[json_response_error_key] json_response.delete(jwt_access_token_key) else json_response[jwt_access_token_key] = token end end def jwt_session_hash h = super t = Time.now.to_i h[:exp] = t + jwt_access_token_period h[:iat] = t h[:nbf] = t - jwt_access_token_not_before_period h end def account_from_refresh_token(token) @account = _account_from_refresh_token(token) end private def rescue_jwt_payload(e) if e.instance_of?(JWT::ExpiredSignature) begin # Some versions of jwt will raise JWT::ExpiredSignature even when the # JWT is invalid for other reasons. Make sure the expiration is the # only reason the JWT isn't valid before treating this as an expired token. JWT.decode(jwt_token, jwt_secret, true, Hash[jwt_decode_opts].merge!(:verify_expiration=>false, :algorithm=>jwt_algorithm))[0] rescue => e else json_response[json_response_error_key] = expired_jwt_access_token_message response.status ||= expired_jwt_access_token_status end end super end def _account_from_refresh_token(token) id, token_id, key = _account_refresh_token_split(token) unless key && (id == session_value.to_s) && (actual = get_active_refresh_token(id, token_id)) && timing_safe_eql?(key, convert_token_key(actual)) && jwt_refresh_token_match?(key) return end ds = account_ds(id) ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks? ds.first end def _account_refresh_token_split(token) id, token = split_token(token) return unless id && token token_id, key = split_token(token) return unless token_id && key [id, token_id, key] end def _jwt_decode_opts if allow_refresh_with_expired_jwt_access_token? && request.path == jwt_refresh_path Hash[super].merge!(:verify_expiration=>false) else super end end def jwt_refresh_token_match?(key) # We don't need to match tokens if we are requiring a valid current access token return true unless allow_refresh_with_expired_jwt_access_token? # If allowing with expired jwt access token, check the expired session contains # hmac matching submitted and active refresh token. timing_safe_eql?(compute_hmac(session[jwt_refresh_token_data_session_key].to_s + key), session[jwt_refresh_token_hmac_session_key].to_s) end def get_active_refresh_token(account_id, token_id) jwt_refresh_token_account_ds(account_id). where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column). delete jwt_refresh_token_account_token_ds(account_id, token_id). get(jwt_refresh_token_key_column) end def jwt_refresh_token_account_ds(account_id) jwt_refresh_token_ds.where(jwt_refresh_token_account_id_column => account_id) end def jwt_refresh_token_account_token_ds(account_id, token_id) jwt_refresh_token_account_ds(account_id). where(jwt_refresh_token_id_column=>token_id) end def jwt_refresh_token_ds db[jwt_refresh_token_table] end def remove_jwt_refresh_token_key(token) account_id, token_id, _ = _account_refresh_token_split(token) jwt_refresh_token_account_token_ds(account_id, token_id).delete end def generate_refresh_token hash = jwt_refresh_token_insert_hash [account_id, jwt_refresh_token_ds.insert(hash), convert_token_key(hash[jwt_refresh_token_key_column])].join(token_separator) end def jwt_refresh_token_insert_hash hash = {jwt_refresh_token_account_id_column => account_id, jwt_refresh_token_key_column => random_key} set_deadline_value(hash, jwt_refresh_token_deadline_column, jwt_refresh_token_deadline_interval) hash end def set_jwt_refresh_token_hmac_session_key(token) if allow_refresh_with_expired_jwt_access_token? key = _account_refresh_token_split(token).last data = random_key set_session_value(jwt_refresh_token_data_session_key, data) set_session_value(jwt_refresh_token_hmac_session_key, compute_hmac(data + key)) end end def before_logout if token = param_or_nil(jwt_refresh_token_key_param) if token == 'all' jwt_refresh_token_account_ds(session_value).delete else id, token_id, key = _account_refresh_token_split(token) if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual)) jwt_refresh_token_account_token_ds(id, token_id).delete end end end super if defined?(super) end def after_close_account jwt_refresh_token_account_ds(account_id).delete super if defined?(super) end end end