00:00:10.880
Hi everyone, thank you all for joining me here. I really appreciate it.
00:00:15.120
This is my first RailsConf and also my first conference talk ever, so I hope you enjoy this. My name is Leonardo Tegon, but most people call me Tegon, which is how you can find me on GitHub.
00:00:21.960
I don’t tweet much, but when I do, my handle is @t_bone_l. These slides are going to be available on Speaker Deck, so you can check them out later. This is also my first time in the West, as I'm from Brazil. I live in São Paulo and I work at Tata. If you don’t know us, we are a software development and agile consultancy. You may have heard of us from our open-source work, as we are the ones behind the Legacy language and we have created some Ruby gems like Simple Form and Devise.
00:00:43.200
Devise is an authentication gem, and since authentication is a very common feature in web applications, it's beneficial to have libraries that handle it for us, so we don't have to code it from scratch. All it takes to have Devise working today is running a couple of generators, and that’s it.
00:01:10.680
Well, here’s the thing: those generators can only get us to a certain point. Hopefully, if your project succeeds, you will likely need different requirements for authentication. So, when the time comes, it’s good to know how the gem we are using works, so we can create the customizations we need without much trouble.
00:01:30.450
For example, I was working on a project where we had token-based authentication for a JSON API. This is something Devise doesn’t support out of the box. We ended up overriding the entire Devise status controller. When we have specific requirements like this one, we can rely directly on Warden's features to build a custom authentication logic. Today, we will see how to do this, and hopefully this can be useful for you in the future.
00:02:06.060
Devise has a lot of features, including email confirmation, password recovery, account locking, tracking, and so on. However, when we are talking about authentication, Warden is the one doing the job. Warden takes care of signing a user in or out, session management, and much more.
00:02:21.849
So, what exactly is Warden? When I hear the word, the first thing that comes to my mind is the Warden of the North. But I’m not here to talk about Game of Thrones! Here, we are focused on a Ruby gem. Warden is a Rack middleware that provides a mechanism for authentication in Ruby web applications.
00:02:37.690
To understand what Warden is, it's essential to know what Rack is. Rack is the Ruby interface for web services, unifying the API for both web services and web frameworks in Ruby. When we start a request from a browser to our Rails application, it goes through a web server (like Puma), then down through the Rails application in a format that conforms to the Rack API. The same is true for the response. Because Rack unifies the request and response API, we can replace Puma with Unicorn or Passenger, and everything still works.
00:03:11.400
Now, let’s see how we can implement a Rack API in an application. It’s pretty easy to do this; we create a class that responds to a method call. This method receives one argument, and we have to return an array with three items: the first one is the response status, the second is the response headers, and the third is the response body. This is pretty much how you implement a Rack API.
00:03:34.510
In this context, the argument we receive is the request environment. It is a hash that contains all the information related to the current request. For instance, it includes the server protocol, request method, request path, and so on. As I mentioned before, Warden is a Rack middleware. Any middleware looks like a Rack application, but here we are actually receiving a Rack application instance which allows us to change their environment hash in some way.
00:04:02.590
As a middleware, they can stop the request execution. For example, when we start a request, we can pass through a login middleware. At this point, Warden will say: 'Hey, in order to continue, a user has to be authenticated.' If the user is not authenticated, we throw a 401 Unauthorized error. The other middleware down the stack will not be called.
00:04:27.950
Going back to our Rack application example, the idea here is to implement these concepts throughout the presentation. By the end of it, we will have a simple authentication system working with Warden. You can follow along and refer to the slides later. Just remember to save this file and name it 'config.ru' as a comment at the top is provided.
00:04:57.400
To run a Rack application, you can use the 'rackup' command. Once we send a request, it’s important to notice that Warden doesn’t handle session storage directly; it has to run after a session middleware. Let's say we pass through a session cookie middleware. This middleware is going to create an object in the environment hash called `warden.session`.
00:05:15.840
When Warden gets this middleware, it uses this session object. Warden provides methods for authenticating a user and checking if a user is authenticated by calling methods on this object. To use Warden in a Rack application, we have to first configure which middleware we want to use. Here, we are using the session cookie middleware and the Warden middleware. It's essential to note that the order matters; we have to declare Warden after the session cookie middleware.
00:05:48.890
Another thing we need to do is say how the user is going to be serialized into and from the session. In this example, we simply serialize the user ID. We keep the ID later to find this user. Here’s a basic example with two users: Bruce Wayne and James Gordon. Please note that I am using plaintext passwords here just to keep the example simple.
00:06:06.510
Now back to our setup. The 'strategy' setting is important to understand because a strategy is where we put the logic to authenticate a request. If we use token authentication, we might try to find a user based on a given token or, for example, an email and check whether the password is valid or not.
00:06:26.410
In this example, we’re going to use a plain password strategy. We define the strategy by calling the `add` method on the Warden strategies class and passing an argument to identify it. In our case, we call it 'password'. We define two methods: first, we have 'valid', which returns whether a strategy should run or not, and we also need to define an 'authenticate' method.
00:06:57.330
Here, we try to find the user based on the provided email and password. If we find the user, we call 'success' and pass the user as an argument. If it doesn’t match, we call 'fail' and provide an invalid email or password message. The failure method drops into the Warden object under the message method, which you can use in your failure application.
00:07:17.680
Now, let’s add some encryption using the BCrypt Ruby gem. It’s easy to use; we just call the `create` method on the password class and pass in a string. This shows how Bruce Wayne's supersecret password is stored. Now in our users array, we need to change our strategy to perform authentication in two steps.
00:07:40.230
First, we find a user for the email; if the user doesn’t exist, we fail right away. If the user exists, we check whether the password is valid by calling the `is_password?` method from the BCrypt gem. If it returns true, we call success as before, and if we test again, everything should still work, but we are now using encrypted passwords.
00:08:14.660
Moving on to scopes, we have already used scopes, but let's clarify with an example. Imagine an e-commerce application. Customers can search for items or complete purchases, while editors manage those items. We need a person to be both a customer and an editor.
00:08:27.770
To solve this, we can use different tables or modules for customers and editors. Device allows us to declare multiple modules, with each module using a separate Warden scope. What we pass to the device method will be considered a Warden scope, and it’s used in many places within Device.
00:08:45.700
As an example, if we sign in as an editor, our session will hold information specific to that role, while signing in as a customer gives us that session context. This allows both users to coexist in the same application, providing us with the functionality we need.
00:09:16.480
When using Warden, we don't have to configure anything special; we just pass the scope as an option to the authenticate method. If we don’t provide a scope, the default one is used. This also applies when we call methods like 'authenticated' to verify if a user is logged in or to fetch the user from session data.
00:09:41.710
For logout, if we don’t pass any arguments, Warden will log out all scopes. It’s important to specify which scope you want to log out if that’s your intention. Finally, we can add some configurations for scopes, setting which scope is treated as the default for the application, and we can define strategies used for a specific scope.
00:10:09.610
Warden supports callbacks for authentication events. For instance, we have 'after set user', which gets called during three situations: when we fetch the user from the session, authenticate the user, or directly set the user.
00:10:39.420
As arguments, we receive the Warden instance (similar to the Warden object in the environment hash) and a hash of options. There are various uses for these callbacks inside Device, like checking if a user’s email is confirmed or creating the 'remember me' cookie right after signing in.
00:11:10.760
We also have 'on request', which gets called on each request after Warden has initialized. The 'before failure' callback is useful for modifying the environment hash after a failed authentication attempt.
00:11:35.210
With 'before logout', we use the same arguments as before. This is particularly useful for the 'remember me' feature, as we need to delete the cookie after the user logs out. It's noteworthy that callbacks run sequentially. If a particular callback needs to run first, we can prefix its name with 'prepared_'. This prefixing will ensure that it executes before others.
00:12:04.040
To recap, we’ve seen that Warden is a Rack-based middleware for authentication. It provides helpers for signing users in, signing them out, and managing sessions seamlessly. We learned how to implement custom authentication logic using strategies and handle authentication failures gracefully.
00:12:25.510
We also saw how to use scopes to manage multiple user types within the same session. Finally, we explored callbacks provided by Warden for authentication events and how we can leverage them effectively.
00:12:51.150
Understanding how Warden and Device work brings numerous benefits. It not only helps solve bugs more confidently, but it also enables efficient customization. For instance, when defining token-based authentication, you can create a custom Warden strategy rather than overriding the entire Device setup.
00:13:16.650
If you’re interested in contributing to Devise, feel free to reach out to me after the talk. I’ll be happy to help you with everything you need.
00:13:47.010
Thank you!