Introduction to authentication
Marten allows the generation of new projects with a built-in authentication application that handles basic user management needs. You can then extend and adapt this application so that it accommodates your project's needs.
Overview
Marten's new
management command allows the generation of projects with a built-in auth
application. This application is part of the created project: it provides the necessary models, handlers, schemas, emails, and templates allowing to authenticate users with email addresses and passwords, while also supporting standard password reset flows. On top of that, an Auth::User
model is automatically generated for your newly created projects. Since this model is also part of your project, this means that it's possible to easily add new fields to it and generate migrations for it as well.
Here is the list of responsibilities of the generated authentication application:
- Signing up users
- Signing in users
- Signing out users
- Allowing users to reset their passwords
- Allowing users to access a basic profile page
Internally, this authentication application relies on the official marten-auth
shard. This shard implements low-level authentication operations (such as authenticating user credentials, generating securely encrypted passwords, generating password reset tokens, etc).
Generating projects with authentication
Generating new projects with authentication can be easily achieved by leveraging the --with-auth
option of the new
management command.
For example:
marten new project myblog --with-auth
When using this option, Marten will generate an auth
application under the src/auth
folder of your project. As mentioned previously, this application provides a set of models, handlers, schemas, emails, and templates that implement basic authentication operations.
You can test the generated authentication application by going to your application at http://localhost:8000/auth/signup after having started the Marten development server (using marten serve
).
You can see the full list of files generated for the auth
application in Generated files.
Usage
This section covers the basics of how to use the auth
application - powered by marten-auth
- that is generated when creating projects with the --with-auth
option.
The User
model
The auth
application defines a single Auth::User
model that inherits its fields from the abstract MartenAuth::User
model. As such, this model automatically provides the following fields:
id
- abig_int
field containing the primary key of the useremail
- anemail
field containing the user's email addresspassword
- astring
field containing the user's encrypted passwordcreated_at
- adate_time
field containing the user creation dateupdated_at
- adate_time
field containing the last user modification date
Retrieving the current user
Projects that are generated with the auth
application automatically make use of a middleware (MartenAuth::Middleware
) that ensures that the currently authenticated user ID is associated with the current request. This means that given a specific HTTP request (instance of Marten::HTTP::Request
), it is possible to identify which user is signed-in or not. Concretely, the following methods are made available on the standard Marten::HTTP::Request
object in order to interact with the currently signed-in user:
Method | Description |
---|---|
#user_id | Returns the current user ID associated with the considered request, or nil if there is no authenticated user. |
#user | Returns the user associated with the request, or nil if there is no authenticated user. |
#user! | Returns the user associated with the request, or raise NilAssertionError if there is no authenticated user. |
#user? | Returns true if a user is authenticated for the request. |
This makes it possible to easily check whether a user is authenticated in handlers in order to implement different logic. For example:
class MyHandler < Marten::Handler
def get
if request.user?
respond "User ##{request.user!.id} is signed-in"
else
respond "No signed-in user"
end
end
end
Creating users
Creating a user is as simple as initializing an instance of the Auth::User
model and defining its properties. That being said it is important to note that the user's password (password
field) must be set using the #set_password
method: this will ensure that the raw password you provide to this method is properly encrypted and that the resulting hash is assigned to the password
field. Because of this, you should not attempt to manipulate the password
field attribute of user records directly.
For example:
user = Auth::User.new(email: "[email protected]") do |user|
user.set_password("insecure")
end
user.save!
Authenticating users
Authentication is the act of verifying a user's credentials. This capability is provided by the marten-auth
shard through the use of the MartenAuth#authenticate
method: this method tries to authenticate the user associated identified by a natural key (typically, an email address) and check that the given raw password is valid. The method returns the corresponding user record if the authentication is successful. Otherwise, it returns nil
if the credentials can't be verified because the user does not exist or because the password is invalid.
For example:
user = MartenAuth.authenticate("[email protected]", "insecure")
if user
puts "User credentials are valid!"
else
puts "User credentials are not valid!"
end
It is important to realize that this method only verifies user credentials. It does not sign in users for a specific request. Signing in users (and attaching them to the current session) is handled by the #sign_in
method, which is discussed in Signing in users.
The MartenAuth#authenticate
method is automatically used by the handlers that are generated for your auth
application before signing in users.
Signing in users
Signing in a user is the act of attaching it to the current session - after having verified that the associated credentials are valid (see Authenticating users). This capability is provided by the marten-auth
shard through the use of the MartenAuth#sign_in
method: This method takes a request object (instance of Marten::HTTP::Request
) and a user record as arguments and ensures that the user ID is attached to the current session so that they do not have to reauthenticate for every request.
For example:
class MyHandler < Marten::Handler
def post
user = MartenAuth.authenticate(request.data["email"].to_s, request.data["password"].to_s)
if user
MartenAuth.sign_in(request, user)
redirect reverse("auth:profile")
else
redirect reverse("auth:sign_in")
end
end
end
It is important to understand that this method is intended to be used for a user record whose credentials were validated using the #authenticate
method beforehand. See Authenticating users for more details.
Signing out users
The ability to sign out users is provided by the marten-auth
shard through the use of the MartenAuth#sign_out
method: this method takes a request object (instance of Marten::HTTP::Request
) as argument, removes the authenticated user ID from the current request, and flushes the associated session data.
For example:
class MyHandler < Marten::Handler
def get
MartenAuth.sign_out(request)
redirect reverse("auth:sign_in")
end
end
Changing a user's password
The ability to change a user password is provided by the #set_password
method of the Auth::User
model (which is inherited from the MartenAuth::User
abstract class that is provided by the marten-auth
shard).
For example:
use = User.get!(email: "[email protected]")
user.set_password("insecure")
user.save!
Passwords are encrypted using Crypto::Bcrypt
.
As mentioned previously, you should not attempt to manipulate the password
field directly: this field contains the hash value that results from the encryption of the raw password.
Limiting access to signed-in users
Limiting access to signed-in users can easily be achieved by leveraging the #user?
method that is available from Marten::HTTP::Request
objects. Using this method, you can easily implement #before_dispatch
handler callbacks in order to redirect anonymous users to a sign-in page or to an error page.
For example:
class UserProfileHandler < Marten::Handler
before_dispatch :require_signed_in_user
def get
render "auth/profile.html" { user: request.user }
end
private def require_signed_in_user
redirect reverse("auth:sign_in") unless request.user?
end
end
It should be noted that the auth
application generated for your project already contains an Auth::RequireSignedInUser
concern module that you can include in your handlers in order to ensure that they can only be accessed by signed-in users (and that anonymous users are redirected to the sign-in page).
For example:
class UserProfileHandler < Marten::Handler
include Auth::RequireSignedInUser
def get
render "auth/profile.html" { user: request.user }
end
end