api keys: require reauthentication when working with API keys.

Require the user to re-enter their password before they can view,
create, update, or delete their API keys.

This works by tracking the timestamp of the user's last password
re-entry in a `last_authenticated_at` session cookie, and redirecting
the user to a password confirmation page if they haven't re-entered
their password in the last hour.

This is modeled after Github's Sudo mode.
This commit is contained in:
evazion
2021-02-15 00:09:12 -06:00
parent d99985160a
commit 3d01febcf7
7 changed files with 42 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
class ApiKeysController < ApplicationController
before_action :requires_reauthentication
respond_to :html, :json, :xml
def new

View File

@@ -189,6 +189,15 @@ class ApplicationController < ActionController::Base
params.fetch(PolicyFinder.new(record).param_key, {})
end
def requires_reauthentication
return if CurrentUser.user.is_anonymous?
last_authenticated_at = session[:last_authenticated_at]
if last_authenticated_at.blank? || Time.parse(last_authenticated_at) < 60.minutes.ago
redirect_to confirm_password_session_path(url: request.fullpath)
end
end
# Remove blank `search` params from the url.
#
# /tags?search[name]=touhou&search[category]=&search[order]=

View File

@@ -6,6 +6,9 @@ class SessionsController < ApplicationController
@user = User.new
end
def confirm_password
end
def create
name, password, url = params.fetch(:session, params).slice(:name, :password, :url).values
user = SessionLoader.new(request).login(name, password)

View File

@@ -14,6 +14,7 @@ class SessionLoader
if user.present? && user.authenticate_password(password)
session[:user_id] = user.id
session[:last_authenticated_at] = Time.now.utc.to_s
UserEvent.build_from_request(user, :login, request)
user.last_logged_in_at = Time.now
@@ -31,6 +32,7 @@ class SessionLoader
def logout
session.delete(:user_id)
session.delete(:last_authenticated_at)
return if CurrentUser.user.is_anonymous?
UserEvent.create_from_request!(CurrentUser.user, :logout, request)
end

View File

@@ -0,0 +1,17 @@
<% page_title "Confirm password" %>
<%= render "secondary_links" %>
<div id="c-sessions">
<div id="a-confirm-password">
<h1>Confirm password</h1>
<p>You must re-enter your password to continue.</p>
<%= simple_form_for(:session, url: session_path) do |f| %>
<%= f.input :url, as: :hidden, input_html: { value: params[:url] } %>
<%= f.input :name, as: :hidden, input_html: { value: CurrentUser.user.name } %>
<%= f.input :password, hint: link_to("Forgot password?", password_reset_path), input_html: { autocomplete: "password" } %>
<%= f.submit "Continue" %>
<% end %>
</div>
</div>

View File

@@ -225,6 +225,7 @@ Rails.application.routes.draw do
resources :robots, only: [:index]
resources :saved_searches, :except => [:show]
resource :session, only: [:new, :create, :destroy] do
get :confirm_password, on: :collection
get :sign_out, on: :collection
end
resource :source, :only => [:show]

View File

@@ -35,6 +35,15 @@ class ApiKeysControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_nil response.parsed_body.first["key"]
end
should "redirect to the confirm password page if the user hasn't recently authenticated" do
post session_path, params: { name: @user.name, password: @user.password }
travel_to 2.hours.from_now do
get user_api_keys_path(@user.id)
end
assert_redirected_to confirm_password_session_path(url: user_api_keys_path(@user.id))
end
end
context "#new action" do