Skip to main content
Version: 0.4

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).

info

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
tip

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.

info

You can see the full list of files generated for the generated authentication application in Generated files.

tip

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 - a big_int field containing the primary key of the user
  • email - an email field containing the user's email address
  • password - a string field containing the user's encrypted password
  • created_at - a date_time field containing the user creation date
  • updated_at - a date_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:

MethodDescription
#user_idReturns the current user ID associated with the considered request, or nil if there is no authenticated user.
#userReturns 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!
tip

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
caution

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.

info

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
caution

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!
info

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