Slide 1

Slide 1 text

Warden the building block behind Devise RailsConf 2018 Leonardo Tegon

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Brazil !

Slide 4

Slide 4 text

consulting and software engineering

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

› bundle add devise › rails generate devise:install › rails generate devise User › rails db:migrate › rails server

Slide 7

Slide 7 text

e-mail confirmation password recovery account registration Devise account locking account tracking session timeout

Slide 8

Slide 8 text

authentication logout session management Warden e-mail confirmation password recovery account registration Devise account locking account tracking session timeout

Slide 9

Slide 9 text

Warden?

Slide 10

Slide 10 text

Warden?

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

“Warden is a Rack-based middleware, designed to provide a mechanism for authentication in Ruby web applications” - Warden’s GitHub Wiki

Slide 13

Slide 13 text

A Ruby interface for web servers

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

A middleware can stop the request execution

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

› curl -i localhost:9292 HTTP/1.1 200 OK Content-Type: application/json Transfer-Encoding: chunked { "text": "Hello, RailsConf!" } › rackup

Slide 36

Slide 36 text

Warden doesn't handle session storage

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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: "[email protected]", password: "123", name: "Bruce Wayne" }, { id: 2, email: "[email protected]", password: "321", name: "James Gordon" } ]

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Strategies

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Methods available inside strategies • params – Request parameters • request – Rack::Request object • session – The session object for the request • env – Rack’s request environment hash

Slide 57

Slide 57 text

• 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

Slide 58

Slide 58 text

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)

Slide 59

Slide 59 text

# 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

Slide 60

Slide 60 text

# 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

Slide 61

Slide 61 text

# 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

Slide 62

Slide 62 text

# 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

Slide 63

Slide 63 text

# 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

Slide 64

Slide 64 text

# 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

Slide 65

Slide 65 text

# 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

Slide 66

Slide 66 text

# 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

Slide 67

Slide 67 text

# 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

Slide 68

Slide 68 text

# 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

Slide 69

Slide 69 text

# 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

Slide 70

Slide 70 text

# 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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

Failure applications

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

› curl -i "localhost:[email protected]&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" }

Slide 82

Slide 82 text

› curl -i "localhost:9292? [email protected]&password=abc" HTTP/1.1 401 Unauthorized Content-Type: application/json Transfer-Encoding: chunked Something went wrong

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

› curl -i "localhost:9292? [email protected]&password=abc" HTTP/1.1 401 Unauthorized Content-Type: application/json Transfer-Encoding: chunked Something went wrong: Invalid email or password

Slide 91

Slide 91 text

bcrypt-ruby https://github.com/codahale/bcrypt-ruby

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

USERS = [ { id: 1, email: '[email protected]', password: '$2a$10$eyNdqvloKRe0hlzSz5Q7VOSKTMQq9PpmegqgYNMBbe0.fRxVskP76', name: 'Bruce Wayne' }, { id: 2, email: '[email protected]', password: '$2a$10$alIdvOG32Z11Brn9XuX.FuDmoCyEQDLUQEWc/PdoaHvaGDjghp6ZO', name: 'James Gordon' } ]

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

› curl -i "localhost:[email protected]&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" }

Slide 103

Slide 103 text

Scopes

Slide 104

Slide 104 text

e-commerce

Slide 105

Slide 105 text

e-commerce search wishlist purchases customers

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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)

Slide 113

Slide 113 text

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)

Slide 114

Slide 114 text

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)

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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}

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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}

Slide 120

Slide 120 text

# 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

Slide 121

Slide 121 text

Using scopes # logs everyone out env["warden"].logout # logs :default scope out env["warden"].logout(:default) # logs :customer out env[“warden"].logout(:customer)

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

Callbacks

Slide 124

Slide 124 text

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)

Slide 125

Slide 125 text

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=>"[email protected]", :password=>"$2a$10$eyNdqvloKRe0hlzS z5Q7VOSKTMQq9PpmegqgYNMBbe0.fRxVskP 76", :name=>"Bruce Wayne"}

Slide 126

Slide 126 text

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)

Slide 127

Slide 127 text

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=>#}

Slide 128

Slide 128 text

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)

Slide 129

Slide 129 text

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}

Slide 130

Slide 130 text

# 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

Slide 131

Slide 131 text

# 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

Slide 132

Slide 132 text

# 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

Slide 133

Slide 133 text

# 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

Slide 134

Slide 134 text

# 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

Slide 135

Slide 135 text

# 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

Slide 136

Slide 136 text

# 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

Slide 137

Slide 137 text

# 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

Slide 138

Slide 138 text

# 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

Slide 139

Slide 139 text

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)

Slide 140

Slide 140 text

on_request Warden::Manager.on_request do |warden| # do something end

Slide 141

Slide 141 text

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=>#}

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

before_failure Warden::Manager.before_failure do |env, opts| # do something end {:action=>"unauthenticated", :message=>"Invalid email or password", :attempted_path=>"/? [email protected]&password=1234"}

Slide 146

Slide 146 text

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|

Slide 147

Slide 147 text

# 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

Slide 148

Slide 148 text

# 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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

Review

Slide 151

Slide 151 text

Review • Warden is a Rack-based middleware for authentication

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

Abstractions help us build features ✨

Slide 158

Slide 158 text

But it’s good to learn how our tools work

Slide 159

Slide 159 text

Benefits of knowing how a Gem works

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

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

Slide 162

Slide 162 text

Benefits of knowing how a Gem works •Chase down bugs with confidence •Customize their behavior efficiently •Contribute to open source ❤

Slide 163

Slide 163 text

Thank you! github.com/tegon twitter.com/tegonl speakerdeck.com/tegon RailsConf 2018