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. The same application can also be added to existing projects by leveraging the auth
generator.
The generated authentication application is part of your 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/apps/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.
Adding authentication to existing projects
The auth
generator can be leveraged in order to add an authentication application to an existing project.
For example, the following command will add a new authentication app with the auth
label to the current project:
marten gen auth
Note that you can also customize the label given to the generated authentication app by providing an additional argument containing the intended app label:
marten gen auth my_auth
This generator will add an authentication application under your project's src
folder (or src/apps
folder if it is defined). As mentioned previously, this application provides a set of models, handlers, schemas, emails, and templates that implement basic authentication operations.
Note that the generator will also add the generated application to the installed_apps
setting and will also configure Crystal requirements for it (in the src/project.cr
and src/cli.cr
files). It will also add authentication-related settings to your base settings file and will add the marten-auth
shard to your project's shard.yml
automatically.
You can see the full list of files generated for the generated authentication application in Generated files.
Don't forget to run marten migrate
after the authentication app has been generated so that your user model gets created at the database level. You should also check the config/routes.cr
file or run the marten routes
management command to see the routes associated with your generated authentication app.
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!
In certain scenarios where passwords aren't required, such as with email magic link authentication or multi-provider OAuth authentication, it may be desirable to create user accounts without passwords. To achieve this, you can utilize the #set_unusable_password
method on your user model instances. This method ensures that no password can be used for these accounts.
For example:
user = Auth::User.new(email: "[email protected]") do |user|
user.set_unusable_password
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