Warden: the building block behind Devise

Warden: the building block behind Devise

Authentication is one of the most common features of web applications, so it makes sense to have libraries that provide solutions for this problem. You've probably heard of or maybe used Devise: all you have to do is add it to your Gemfile and run a generator, and you have a robust authentication system.

Behind the scenes, Devise uses Warden to handle authentication. In this talk, I'll explain what Warden is, why it's useful and how Devise takes advantage of it to build the most popular authentication gem for Rails.

534f215d73d93dae9314db40d9746acd?s=128

Leonardo Tegon

April 17, 2018
Tweet

Transcript

  1. Warden the building block behind Devise RailsConf 2018 Leonardo Tegon

  2. Leonardo Tegon github.com/tegon twitter.com/tegonl speakerdeck.com/tegon RailsConf 2018

  3. Brazil !

  4. consulting and software engineering

  5. None
  6. › bundle add devise › rails generate devise:install › rails

    generate devise User › rails db:migrate › rails server
  7. e-mail confirmation password recovery account registration Devise account locking account

    tracking session timeout
  8. authentication logout session management Warden e-mail confirmation password recovery account

    registration Devise account locking account tracking session timeout
  9. Warden?

  10. Warden?

  11. None
  12. “Warden is a Rack-based middleware, designed to provide a mechanism

    for authentication in Ruby web applications” - Warden’s GitHub Wiki
  13. A Ruby interface for web servers

  14. GET https://github.com/plataformatec/devise

  15. GET https://github.com/plataformatec/devise

  16. GET https://github.com/plataformatec/devise

  17. GET https://github.com/plataformatec/devise

  18. GET https://github.com/plataformatec/devise

  19. GET https://github.com/plataformatec/devise

  20. class MyApp def call(env) status = 200 headers = {

    "Content-Type" => "application/json" } body = ['{ "text": "Hello, RailsConf!" }'] [status, headers, body] end end run MyApp.new Rack application
  21. class MyApp def call(env) status = 200 headers = {

    "Content-Type" => "application/json" } body = ['{ "text": "Hello, RailsConf!" }'] [status, headers, body] end end run MyApp.new { "rack.version"=>[1, 3], "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"puma 3.11.0 Love Song", "GATEWAY_INTERFACE"=>"CGI/1.2", "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"localhost:9292", "HTTP_USER_AGENT"=>"curl/7.54.0", "HTTP_ACCEPT"=>"*/*", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292" } Rack application
  22. Rack middleware class MyMiddleware def initialize(app) @app = app end

    def call(env) # do something with env @app.call(env) end end
  23. Rack middleware class MyMiddleware def initialize(app) @app = app end

    def call(env) # do something with env @app.call(env) end end #<MyApp:0x007fb8972a39e0>
  24. GET https://github.com/plataformatec/devise

  25. Rails::Rack::Logger GET https://github.com/plataformatec/devise

  26. Rails::Rack::Logger ActionDispatch::Cookies GET https://github.com/plataformatec/devise

  27. Rails::Rack::Logger ActionDispatch::Cookies ActionDispatch::Flash GET https://github.com/plataformatec/devise

  28. A middleware can stop the request execution

  29. GET https://github.com/plataformatec/devise

  30. Rails::Rack::Logger GET https://github.com/plataformatec/devise

  31. Rails::Rack::Logger Warden::Manager GET https://github.com/plataformatec/devise

  32. Rails::Rack::Logger Warden::Manager GET https://github.com/plataformatec/devise 401 unauthorized

  33. Rails::Rack::Logger Warden::Manager GET https://github.com/plataformatec/devise 401 unauthorized

  34. Rack app example # config.ru class MyApp def call(env) status

    = 200 headers = { "Content-Type" => "application/json" } body = ['{ "text": "Hello, RailsConf!" }'] [status, headers, body] end end run MyApp.new
  35. › curl -i localhost:9292 HTTP/1.1 200 OK Content-Type: application/json Transfer-Encoding:

    chunked { "text": "Hello, RailsConf!" } › rackup
  36. Warden doesn't handle session storage

  37. GET https://github.com/plataformatec/devise

  38. GET https://github.com/plataformatec/devise Rack::Session::Cookie

  39. GET https://github.com/plataformatec/devise Rack::Session::Cookie env["rack.session"]

  40. GET https://github.com/plataformatec/devise Rack::Session::Cookie Warden::Manager env["rack.session"]

  41. GET https://github.com/plataformatec/devise Rack::Session::Cookie Warden::Manager env["rack.session"] env["warden"]

  42. use Rack::Session::Cookie, secret: "warden" use Warden::Manager do |manager| manager.default_strategies :password

    manager.failure_app = FailureApp.new end run MyApp.new Warden setup
  43. Session serialization Warden::Manager.serialize_into_session do |user| user[:id] end Warden::Manager.serialize_from_session do |id|

    USERS.find { |user| user[:id] == id } end
  44. Session serialization Warden::Manager.serialize_into_session do |user| user[:id] end Warden::Manager.serialize_from_session do |id|

    USERS.find { |user| user[:id] == id } end USERS = [ { id: 1, email: "bruce@wayne.com", password: "123", name: "Bruce Wayne" }, { id: 2, email: "gordon@gcpd.gov", password: "321", name: "James Gordon" } ]
  45. use Rack::Session::Cookie, secret: "warden" use Warden::Manager do |manager| manager.default_strategies :password

    manager.failure_app = FailureApp.new end run MyApp.new Warden setup
  46. use Rack::Session::Cookie, secret: "warden" use Warden::Manager do |manager| manager.default_strategies :password

    manager.failure_app = FailureApp.new end run MyApp.new Warden setup
  47. Strategies

  48. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  49. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  50. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  51. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  52. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  53. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  54. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  55. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  56. Methods available inside strategies • params – Request parameters •

    request – Rack::Request object • session – The session object for the request • env – Rack’s request environment hash
  57. • success! – Logs a user in (causes a halt!)

    • fail! – Sets the strategy to fail (causes a halt!) • halt! – Makes this one the last strategy processed • redirect! – Redirects to another url (causes a halt!) • custom! – Accepts a custom rack array (causes a halt!) • pass – Ignore this strategy (default) Methods available inside strategies
  58. Using a strategy # authenticates with the default strategy env["warden"].authenticate

    # authenticates with the `password` strategy env["warden"].authenticate(:password) # try to authenticate with the `password` strategy. # If it fails, authenticate with the `api_token` strategy env["warden"].authenticate(:password, :api_token)
  59. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  60. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  61. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  62. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  63. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  64. # devise/lib/devise/strategies/rememberable.rb def valid? @remember_cookie = nil remember_cookie.present? end def

    authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end devise sample
  65. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  66. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  67. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  68. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  69. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  70. # devise/lib/devise/strategies/database_authenticatable.rb def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed

    = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end mapping.to.new.password = password if !hashed && Devise.paranoid fail(:not_found_in_database) unless resource end devise sample
  71. use Rack::Session::Cookie, secret: "warden" use Warden::Manager do |manager| manager.default_strategies :password

    manager.failure_app = FailureApp.new end run MyApp.new Warden setup
  72. use Rack::Session::Cookie, secret: "warden" use Warden::Manager do |manager| manager.default_strategies :password

    manager.failure_app = FailureApp.new end run MyApp.new Warden setup
  73. Failure applications

  74. class FailureApp def call(env) status = 401 headers = {

    "Content-Type" => "application/json" } body = ["Something went wrong"] [status, headers, body] end end
  75. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  76. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  77. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  78. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  79. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  80. Requiring authentication require "warden" class MyApp def call(env) env["warden"].authenticate! user

    = env["warden"].user status = 200 headers = { "Content-Type" => "application/json" } body = ["{ \"user\": \"#{user[:name]}\" }"] [status, headers, body] end end
  81. › curl -i "localhost:9292?email=bruce@wayne.com&password=123" HTTP/1.1 200 OK Content-Type: application/json Set-Cookie:

    rack.session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiRWE3ZjA3MTkzNTY2MG U1ZmE1MzZh%0AZWY5M2QzZmNjNTg1MTVhNmYwZmNmNDQyMzFiZWExNzNkOGZkND RiNmU2NTcG%0AOwBGSSIcd2FyZGVuLnVzZXIuZGVmYXVsdC5rZXkGOwBUaQY%3D %0A--ac7c6d8d93aba354ad6cff80c59ef0d1730f8938; path=/; HttpOnly Transfer-Encoding: chunked { "user": "Bruce Wayne" }
  82. › curl -i "localhost:9292? email=bruce@wayne.com&password=abc" HTTP/1.1 401 Unauthorized Content-Type: application/json

    Transfer-Encoding: chunked Something went wrong
  83. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  84. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end
  85. email & password strategy Warden::Strategies.add(:password) do def valid? params["email"] &&

    params["password"] end def authenticate! user = USERS.find do |user| user[:email] == params["email"] && user[:password] == params["password"] end if user.nil? fail!("Invalid email or password") else success!(user) end end end env["warden"].message
  86. class FailureApp def call(env) error_message = env["warden"].message status = 401

    headers = { "Content-Type" => "application/json" } body = ["Something went wrong: #{error_message}"] [status, headers, body] end end
  87. class FailureApp def call(env) error_message = env["warden"].message status = 401

    headers = { "Content-Type" => "application/json" } body = ["Something went wrong: #{error_message}"] [status, headers, body] end end
  88. class FailureApp def call(env) error_message = env["warden"].message status = 401

    headers = { "Content-Type" => "application/json" } body = ["Something went wrong: #{error_message}"] [status, headers, body] end end
  89. class FailureApp def call(env) error_message = env["warden"].message status = 401

    headers = { "Content-Type" => "application/json" } body = ["Something went wrong: #{error_message}"] [status, headers, body] end end
  90. › curl -i "localhost:9292? email=bruce@wayne.com&password=abc" HTTP/1.1 401 Unauthorized Content-Type: application/json

    Transfer-Encoding: chunked Something went wrong: Invalid email or password
  91. bcrypt-ruby https://github.com/codahale/bcrypt-ruby

  92. Encrypting a password > BCrypt::Password.create("123") => "$2a$10$eyNdqvloKRe0hlzSz5Q7VOSKTMQq9PpmegqgYNMBb e0.fRxVskP76"

  93. USERS = [ { id: 1, email: 'bruce@wayne.com', password: '$2a$10$eyNdqvloKRe0hlzSz5Q7VOSKTMQq9PpmegqgYNMBbe0.fRxVskP76',

    name: 'Bruce Wayne' }, { id: 2, email: 'gordon@gcpd.gov', password: '$2a$10$alIdvOG32Z11Brn9XuX.FuDmoCyEQDLUQEWc/PdoaHvaGDjghp6ZO', name: 'James Gordon' } ]
  94. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  95. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  96. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  97. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  98. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  99. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  100. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  101. def authenticate! user = USERS.find { |user| user[:email] == params["email"]

    } return fail!("Invalid email or password") if user.nil? if BCrypt::Password.new(user[:password]).is_password?(params["password"]) success!(user) else fail!("Invalid email or password") end end
  102. › curl -i "localhost:9292?email=bruce@wayne.com&password=123" HTTP/1.1 200 OK Content-Type: application/json Set-Cookie:

    rack.session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiRWRkZGY1NzBkMDM1 YTE5OTI0OGI5%0AMWU5NWM2NTdlZjAxN2QwZjE2NmNjZWU2YTNkOWFiZmI0Zj c5Y2E5MmI4OTAG%0AOwBGSSIcd2FyZGVuLnVzZXIuZGVmYXVsdC5rZXkGOwBU aQY%3D%0A--49fb8a3f9f887658cff35ee6c48d93618b044be3; path=/; HttpOnly Transfer-Encoding: chunked { "user": "Bruce Wayne" }
  103. Scopes

  104. e-commerce

  105. e-commerce search wishlist purchases customers

  106. e-commerce search wishlist purchases customers manage items promotions refunds editors

  107. User model with many roles class User < ApplicationRecord has_many

    :roles
  108. User model with many roles class User < ApplicationRecord has_many

    :roles
  109. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query)
  110. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item)
  111. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item) def purchase!(item)
  112. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item) def purchase!(item) def create_item(attributes)
  113. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item) def purchase!(item) def create_item(attributes) def promote_item(item)
  114. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item) def purchase!(item) def create_item(attributes) def promote_item(item) def refund_purchase(order)
  115. User model with many roles class User < ApplicationRecord has_many

    :roles def search_items(query) def add_to_wishlist(item) def purchase!(item) def create_item(attributes) def promote_item(item) def refund_purchase(order) end
  116. Multiple models with devise /editors/sign_in current_editor class Editor < ApplicationRecord

    devise :editors def create_item(attributes) def promote_item(item) def refund_purchase(order) end /customers/sign_in current_customer class Customer < ApplicationRecord devise :customers def search_items(query) def add_to_wishlist(item) def purchase!(item) end
  117. Multiple models with devise /editors/sign_in current_editor class Editor < ApplicationRecord

    devise :editors def create_item(attributes) def promote_item(item) def refund_purchase(order) end /customers/sign_in current_customer class Customer < ApplicationRecord devise :customers def search_items(query) def add_to_wishlist(item) def purchase!(item) end env["rack.session"] => {"warden.user.editor.key"=>1}
  118. Multiple models with devise /editors/sign_in current_editor class Editor < ApplicationRecord

    devise :editors def create_item(attributes) def promote_item(item) def refund_purchase(order) end /customers/sign_in current_customer class Customer < ApplicationRecord devise :customers def search_items(query) def add_to_wishlist(item) def purchase!(item) end
  119. Multiple models with devise /editors/sign_in current_editor class Editor < ApplicationRecord

    devise :editors def create_item(attributes) def promote_item(item) def refund_purchase(order) end /customers/sign_in current_customer class Customer < ApplicationRecord devise :customers def search_items(query) def add_to_wishlist(item) def purchase!(item) end env["rack.session"] => {"warden.user.editor.key"=>1, "warden.user.customer.key"=>2}
  120. # authenticate the :customer scope with the :api_token strategy env["warden"].authenticate(:api_token,

    scope: :customer) # if no scope is passed, :default is used env["warden"].authenticated? # check the :customer scope env["warden"].authenticated?(:customer) # fetch the customer env[“warden"].user(:customer) Using scopes
  121. Using scopes # logs everyone out env["warden"].logout # logs :default

    scope out env["warden"].logout(:default) # logs :customer out env[“warden"].logout(:customer)
  122. Scope configuration use Warden::Manager do |config| config.default_scope = :customer config.scope_defaults(:customer,

    strategies: [:password]) config.scope_defaults( :editor, store: false, strategies: [:editor, :account_owner] ) end
  123. Callbacks

  124. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user)
  125. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user) {:id=>1, :email=>"bruce@wayne.com", :password=>"$2a$10$eyNdqvloKRe0hlzS z5Q7VOSKTMQq9PpmegqgYNMBbe0.fRxVskP 76", :name=>"Bruce Wayne"}
  126. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user)
  127. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user) Warden::Proxy:70215393660020 @config={:default_scope=>:default, :scope_defaults=> {}, :default_strategies=>{:_all=>[:password]}, :inte rcept_401=>true, :failure_app=>#<FailureApp: 0x007fb8972a39e0>}
  128. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user)
  129. after_set_user Warden::Manager.after_set_user do |user, warden, opts| # do something end

    Trigger actions env["warden"].user env["warden"].authenticate env["warden"].set_user(user) {:store=>true, :event=>:authentication, :scope=>:default}
  130. # fetch env["warden"].user # authentication env["warden"].authenticate # set_user env["warden"].set_user(user) Warden::Manager.after_set_user

    except: :fetch Warden::Manager.after_set_user only: :authentication Warden::Manager.after_authentication Warden::Manager.after_set_user only: :fetch Warden::Manager.after_fetch Warden::Manager.after_set_user scope: :admin
  131. # devise/lib/devise/hooks/activatable.rb Warden::Manager.after_set_user do |record, warden, options| if record &&

    record.respond_to?(:active_for_authentication?) && ! record.active_for_authentication? scope = options[:scope] warden.logout(scope) throw :warden, scope: scope, message: record.inactive_message end end devise sample
  132. # devise/lib/devise/hooks/activatable.rb Warden::Manager.after_set_user do |record, warden, options| if record &&

    record.respond_to?(:active_for_authentication?) && ! record.active_for_authentication? scope = options[:scope] warden.logout(scope) throw :warden, scope: scope, message: record.inactive_message end end devise sample
  133. # devise/lib/devise/hooks/activatable.rb Warden::Manager.after_set_user do |record, warden, options| if record &&

    record.respond_to?(:active_for_authentication?) && ! record.active_for_authentication? scope = options[:scope] warden.logout(scope) throw :warden, scope: scope, message: record.inactive_message end end devise sample
  134. # devise/lib/devise/hooks/activatable.rb Warden::Manager.after_set_user do |record, warden, options| if record &&

    record.respond_to?(:active_for_authentication?) && ! record.active_for_authentication? scope = options[:scope] warden.logout(scope) throw :warden, scope: scope, message: record.inactive_message end end devise sample
  135. # devise/lib/devise/hooks/rememberable.rb Warden::Manager.after_set_user except: :fetch do |record, warden, options| scope

    = options[:scope] if record.respond_to?(:remember_me) && options[:store] != false && record.remember_me && warden.authenticated?(scope) Devise::Hooks::Proxy.new(warden).remember_me(record) end end devise sample
  136. # devise/lib/devise/hooks/rememberable.rb Warden::Manager.after_set_user except: :fetch do |record, warden, options| scope

    = options[:scope] if record.respond_to?(:remember_me) && options[:store] != false && record.remember_me && warden.authenticated?(scope) Devise::Hooks::Proxy.new(warden).remember_me(record) end end devise sample
  137. # devise/lib/devise/hooks/trackable.rb Warden::Manager.after_set_user except: :fetch do |record, warden, options| if

    record.respond_to?(:update_tracked_fields!) && warden.authenticated? (options[:scope]) && !warden.request.env['devise.skip_trackable'] record.update_tracked_fields!(warden.request) end end devise sample
  138. # devise/lib/devise/hooks/trackable.rb Warden::Manager.after_set_user except: :fetch do |record, warden, options| if

    record.respond_to?(:update_tracked_fields!) && warden.authenticated? (options[:scope]) && !warden.request.env['devise.skip_trackable'] record.update_tracked_fields!(warden.request) end end devise sample
  139. after_failed_fetch Warden::Manager.after_failed_fetch do |user, warden, opts| Warden::Manager.after_failed_fetch scope: :admin do

    |user, warden, opts| Trigger actions # fails because the user isn't authenticated env["warden"].user env["warden"].user(:admin)
  140. on_request Warden::Manager.on_request do |warden| # do something end

  141. on_request Warden::Manager.on_request do |warden| # do something end Warden::Proxy:70215393660020 @config={:default_scope=>:default,

    :scope_defaults=> {}, :default_strategies=>{:_all=>[:password]}, :inte rcept_401=>true, :failure_app=>#<FailureApp: 0x007fb8972a39e0>}
  142. before_failure Warden::Manager.before_failure do |env, opts| # do something end

  143. before_failure Warden::Manager.before_failure do |env, opts| # do something end {

    "rack.version"=>[1, 3], "QUERY_STRING"=>"email=bruce@wayne.com&password=1234", "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/", "REQUEST_URI"=>"/?email=bruce@wayne.com&password=1234", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292", "PATH_INFO"=>"/unauthenticated", "warden.options"=> { :action=>"unauthenticated", :message=>"Invalid email or password", :attempted_path=>"/?email=bruce@wayne.com&password=1234" } }
  144. before_failure Warden::Manager.before_failure do |env, opts| # do something end

  145. before_failure Warden::Manager.before_failure do |env, opts| # do something end {:action=>"unauthenticated",

    :message=>"Invalid email or password", :attempted_path=>"/? email=bruce@wayne.com&password=1234"}
  146. before_logout Trigger actions env["warden"].logout env["warden"].logout(:admin) Warden::Manager.before_logout do |user, warden, opts|

    Warden::Manager.before_logout scope: :admin do |user, warden, opts|
  147. # devise/lib/devise/hooks/forgetable.rb Warden::Manager.before_logout do |record, warden, options| if record.respond_to?(:forget_me!) Devise::Hooks::Proxy.new(warden).forget_me(record)

    end end devise sample
  148. # devise/lib/devise/hooks/forgetable.rb Warden::Manager.before_logout do |record, warden, options| if record.respond_to?(:forget_me!) Devise::Hooks::Proxy.new(warden).forget_me(record)

    end end devise sample
  149. Prepend callbacks Warden::Manager.prepend_before_logout do |user, warden, opts| # do something

    end
  150. Review

  151. Review • Warden is a Rack-based middleware for authentication

  152. Review • Warden is a Rack-based middleware for authentication •

    A strategy holds the logic to authenticate a request
  153. Review • Warden is a Rack-based middleware for authentication •

    A strategy holds the logic to authenticate a request • A failure application is a Rack endpoint called after authentication failures
  154. Review • Warden is a Rack-based middleware for authentication •

    A strategy holds the logic to authenticate a request • A failure application is a Rack endpoint called after authentication failures • Scopes allow multiple type of users to be logged in at the same time
  155. Review • Warden is a Rack-based middleware for authentication •

    A strategy holds the logic to authenticate a request • A failure application is a Rack endpoint called after authentication failures • Scopes allow multiple type of users to be logged in at the same time • Callbacks are triggered on authentication events
  156. Review • Warden is a Rack-based middleware for authentication •

    A strategy holds the logic to authenticate a request • A failure application is a Rack endpoint called after authentication failures • Scopes allow multiple type of users to be logged in at the same time • Callbacks are triggered on authentication events • Devise examples
  157. Abstractions help us build features ✨

  158. But it’s good to learn how our tools work

  159. Benefits of knowing how a Gem works

  160. Benefits of knowing how a Gem works •Chase down bugs

    with confidence
  161. Benefits of knowing how a Gem works •Chase down bugs

    with confidence •Customize their behavior efficiently
  162. Benefits of knowing how a Gem works •Chase down bugs

    with confidence •Customize their behavior efficiently •Contribute to open source ❤
  163. Thank you! github.com/tegon twitter.com/tegonl speakerdeck.com/tegon RailsConf 2018