320 lines
10 KiB
Ruby
320 lines
10 KiB
Ruby
require 'minitest/autorun'
|
|
require 'prism'
|
|
require 'support'
|
|
require 'tmpdir'
|
|
|
|
class AuthSessionsController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser
|
|
|
|
def create
|
|
user = AuthUser.find_by(email: params['email'])
|
|
if user && user.authenticate(params['password'])
|
|
sign_in(user)
|
|
halt(200, after_authentication_url)
|
|
else
|
|
halt(401, 'invalid credentials')
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
sign_out
|
|
halt(200, 'signed out')
|
|
end
|
|
end
|
|
|
|
class AuthDashboardController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in'
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
class AuthApiController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
class AuthRememberSessionsController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, remember_cookie: :remember_token
|
|
|
|
def create
|
|
user = AuthUser.find_by(email: params['email'])
|
|
if user && user.authenticate(params['password'])
|
|
sign_in(user, remember: params['remember'] == '1')
|
|
halt(200, 'signed in')
|
|
else
|
|
halt(401, 'invalid credentials')
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
sign_out
|
|
halt(200, 'signed out')
|
|
end
|
|
end
|
|
|
|
class AuthRememberDashboardController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in', remember_cookie: :remember_token
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
# `authenticate_with` is declared only on the base class here, the way a
|
|
# real app declares it once on ApplicationController - proving the setting
|
|
# (and its unauthenticated_redirect_path) reaches a subclass that never
|
|
# calls `authenticate_with` itself.
|
|
class AuthBaseController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in'
|
|
end
|
|
|
|
class AuthInheritedController < AuthBaseController
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
class AuthMultiSessionsController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession
|
|
|
|
def create
|
|
user = AuthUser.find_by(email: params['email'])
|
|
if user && user.authenticate(params['password'])
|
|
sign_in(user, remember: params['remember'] == '1')
|
|
halt(200, 'signed in')
|
|
else
|
|
halt(401, 'invalid credentials')
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
sign_out
|
|
halt(200, 'signed out')
|
|
end
|
|
end
|
|
|
|
class AuthMultiDashboardController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in', remember_sessions: :AuthLoginSession
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
|
|
def other_devices
|
|
halt(200, other_login_sessions.count.to_s)
|
|
end
|
|
|
|
def sign_out_others
|
|
sign_out_all_other_sessions
|
|
halt(200, other_login_sessions.count.to_s)
|
|
end
|
|
|
|
def revoke
|
|
# Deliberately the raw String params['id'] gives, not params['id'].to_i -
|
|
# a real controller (see examples/blog's LoginSessionsController) has no
|
|
# reason to convert it itself; revoke_login_session! must do that on its
|
|
# own or every real HTTP request would silently no-op (see its comment).
|
|
ok = revoke_login_session!(params['id'])
|
|
halt(ok ? 200 : 404, ok ? 'revoked' : 'not found')
|
|
end
|
|
end
|
|
|
|
class AuthCappedSessionsController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession, max_remember_sessions: 2
|
|
|
|
def create
|
|
user = AuthUser.find_by(email: params['email'])
|
|
if user && user.authenticate(params['password'])
|
|
sign_in(user, remember: true)
|
|
halt(200, 'signed in')
|
|
else
|
|
halt(401, 'invalid credentials')
|
|
end
|
|
end
|
|
end
|
|
|
|
class AuthCappedDashboardController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in',
|
|
remember_sessions: :AuthLoginSession, max_remember_sessions: 2
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
class AuthZeroCappedSessionsController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, remember_sessions: :AuthLoginSession, max_remember_sessions: 0
|
|
|
|
def create
|
|
user = AuthUser.find_by(email: params['email'])
|
|
if user && user.authenticate(params['password'])
|
|
sign_in(user, remember: true)
|
|
halt(200, 'signed in')
|
|
else
|
|
halt(401, 'invalid credentials')
|
|
end
|
|
end
|
|
end
|
|
|
|
class AuthZeroCappedDashboardController < Prism::Controller
|
|
include Prism::Authenticatable
|
|
authenticate_with model: :AuthUser, unauthenticated_redirect_path: '/sign_in',
|
|
remember_sessions: :AuthLoginSession, max_remember_sessions: 0
|
|
before_action :require_authentication
|
|
|
|
def index
|
|
halt(200, "hello #{current_user.email}")
|
|
end
|
|
end
|
|
|
|
class AuthenticatableApp < Prism::Application
|
|
enable_sessions(secret: 'auth-secret')
|
|
post '/sign_in', to: 'auth_sessions#create'
|
|
post '/sign_out', to: 'auth_sessions#destroy'
|
|
get '/dashboard', to: 'auth_dashboard#index'
|
|
get '/api/me', to: 'auth_api#index'
|
|
get '/inherited/me', to: 'auth_inherited#index'
|
|
post '/remember/sign_in', to: 'auth_remember_sessions#create'
|
|
post '/remember/sign_out', to: 'auth_remember_sessions#destroy'
|
|
get '/remember/dashboard', to: 'auth_remember_dashboard#index'
|
|
post '/multi/sign_in', to: 'auth_multi_sessions#create'
|
|
post '/multi/sign_out', to: 'auth_multi_sessions#destroy'
|
|
get '/multi/dashboard', to: 'auth_multi_dashboard#index'
|
|
get '/multi/other_devices', to: 'auth_multi_dashboard#other_devices'
|
|
post '/multi/sign_out_others', to: 'auth_multi_dashboard#sign_out_others'
|
|
post '/multi/revoke/:id', to: 'auth_multi_dashboard#revoke'
|
|
post '/capped/sign_in', to: 'auth_capped_sessions#create'
|
|
get '/capped/dashboard', to: 'auth_capped_dashboard#index'
|
|
post '/zero-capped/sign_in', to: 'auth_zero_capped_sessions#create'
|
|
get '/zero-capped/dashboard', to: 'auth_zero_capped_dashboard#index'
|
|
end
|
|
|
|
class AuthenticatableTest < Minitest::Test
|
|
include TestSupport
|
|
|
|
def setup
|
|
@dir = Dir.mktmpdir('prism-authenticatable')
|
|
@store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore'))
|
|
@store.create_table(:auth_users, %w[id email password_digest remember_token])
|
|
@store.create_table(:auth_login_sessions, %w[id user_id token user_agent ip_address])
|
|
user_class = Class.new(Prism::Orm::Model) do
|
|
table_name :auth_users
|
|
has_secure_password
|
|
has_secure_token :remember_token
|
|
end
|
|
user_class.store = @store
|
|
Object.const_set(:AuthUser, user_class)
|
|
|
|
login_session_class = Class.new(Prism::Orm::Model) do
|
|
table_name :auth_login_sessions
|
|
has_secure_token :token
|
|
end
|
|
login_session_class.store = @store
|
|
Object.const_set(:AuthLoginSession, login_session_class)
|
|
|
|
AuthUser.create!(email: 'foksiu@example.com', password: 'hunter2')
|
|
end
|
|
|
|
def teardown
|
|
FileUtils.remove_entry(@dir)
|
|
Object.send(:remove_const, :AuthUser) if Object.const_defined?(:AuthUser)
|
|
Object.send(:remove_const, :AuthLoginSession) if Object.const_defined?(:AuthLoginSession)
|
|
end
|
|
|
|
def call(method, path, **opts)
|
|
AuthenticatableApp.call(env_for(method, path, **opts))
|
|
end
|
|
|
|
def session_cookie_from(headers)
|
|
cookie_named(headers, 'prism.session')
|
|
end
|
|
|
|
def cookie_named(headers, name)
|
|
line = headers['Set-Cookie'].split("\n").find { |c| c.start_with?("#{name}=") }
|
|
line&.split(';')&.first
|
|
end
|
|
|
|
def sign_in_cookie
|
|
_, headers, = call('POST', '/sign_in',
|
|
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
|
|
body: 'email=foksiu@example.com&password=hunter2')
|
|
session_cookie_from(headers)
|
|
end
|
|
|
|
def test_wrong_password_is_rejected
|
|
status, = call('POST', '/sign_in',
|
|
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
|
|
body: 'email=foksiu@example.com&password=wrong')
|
|
assert_equal 401, status
|
|
end
|
|
|
|
def test_dashboard_without_session_redirects_to_configured_path
|
|
status, headers, = call('GET', '/dashboard')
|
|
assert_equal 302, status
|
|
assert_equal '/sign_in', headers['Location']
|
|
end
|
|
|
|
def test_api_without_session_halts_with_401
|
|
status, = call('GET', '/api/me')
|
|
assert_equal 401, status
|
|
end
|
|
|
|
def test_sign_in_then_dashboard_sees_current_user
|
|
cookie = sign_in_cookie
|
|
status, _, body = call('GET', '/dashboard', cookies: cookie)
|
|
assert_equal 200, status
|
|
assert_equal 'hello foksiu@example.com', body.join
|
|
end
|
|
|
|
def test_sign_out_forgets_current_user
|
|
cookie = sign_in_cookie
|
|
_, sign_out_headers, = call('POST', '/sign_out', cookies: cookie)
|
|
fresh_cookie = session_cookie_from(sign_out_headers)
|
|
|
|
status, = call('GET', '/dashboard', cookies: fresh_cookie)
|
|
assert_equal 302, status
|
|
end
|
|
|
|
def test_return_to_url_is_remembered_across_the_redirect
|
|
_, headers, = call('GET', '/dashboard')
|
|
refute_nil headers['Location']
|
|
cookie = session_cookie_from(headers)
|
|
|
|
_, sign_in_headers, body = call('POST', '/sign_in',
|
|
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
|
|
cookies: cookie,
|
|
body: 'email=foksiu@example.com&password=hunter2')
|
|
assert_equal '/dashboard', body.join
|
|
refute_nil sign_in_headers['Set-Cookie']
|
|
end
|
|
|
|
def test_authenticate_with_declared_on_a_base_controller_redirects_a_subclass_without_a_session
|
|
status, headers, = call('GET', '/inherited/me')
|
|
assert_equal 302, status
|
|
assert_equal '/sign_in', headers['Location']
|
|
end
|
|
|