pevensie/auth

Pevensie makes it simple to add authentication to your Gleam applications. Currently only email/password authentication is supported, but more authentication methods (OAuth2, passkeys, etc.) are planned for the future.

While you can use Pevensie Auth without the rest of the Pevensie ecosystem, it’s better used as a foundation on which to build a Pevensie-driven application. Other Pevensie modules are designed to integrate well with Pevensie Auth, so you can use them without worrying about authentication.

Getting Started

Pevensie Auth is driver-based, like many other Pevensie modules. This means that you need to choose a driver for your authentication needs. Pevensie provides in-house drivers, but hopefully in the future other drivers will be available.

To get started, you’ll need to create a type to represent your user metadata (see pevensie/user for more details), as well as a decoder and encoder for that type.

import gleam/dynamic.{type DecodeError}
import gleam/json

pub type UserMetadata {
  UserMetadata(name: String, age: Int)
}

pub fn user_metadata_decoder() -> Result(UserMetadata, List(DecodeError)) {
  // ...
}

pub fn user_metadata_encoder(user_metadata: UserMetadata) -> json.Json {
  // ...
}

Next, you’ll need to create a driver of your choice. Here, we’ll be using the first-party Postgres driver, but you can use any driver you like.

import pevensie/drivers/postgres.{type PostgresConfig}

pub fn main() {
  let config = PostgresConfig(
    ..postgres.default_config(),
    database: "my_database",
  )
  let driver = postgres.new_auth_driver(config)
  // ...
}

Now that you have a driver, you can create a Pevensie Auth instance. This instance will act as your entrypoint to the Pevensie Auth API.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  // ...
  let driver = postgres.new_auth_driver(config)
  let pevensie_auth = auth.new(
    driver:,
    user_metadata_decoder:,
    user_metadata_encoder:,
    cookie_key: "super secret signing key",
  )
  // ...
}

Make sure to call connect on your Pevensie Auth instance before using it. This allows your driver to perform any setup required, such as creating a database connection pool. Different drivers may require different setup and at different times (at the start of the application, once per request, etc.). See the documentation for your chosen driver for more information.

Finally, create your first user using create_user_with_email.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  // ...
  let assert Ok(pevensie_auth) = auth.connect(pevensie_auth)
  auth.create_user_with_email(
    pevensie_auth,
    "lucy@pevensie.dev",
    "password",
    UserMetadata(name: "Lucy Pevensie", age: 8),
  )
  // ...
}

Logging In Users

Pevensie Auth provides individual functions for verifying user credentials and creating sessions. However, it also provides a convenience function for logging in users, which will create a session and update the user’s last sign in time.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  // ...
  let assert Ok(#(session, user)) = auth.log_in_user(
    pevensie_auth,
    "lucy@pevensie.dev",
    "password",
    Some(net.parse_ip_address("127.1")),
    None,
  )
  // ...
}

Drivers

Pevensie Auth is designed to be driver-agnostic, so you can use any driver you like. The drivers provided with Pevensie are:

The hope is that other first- and third-party drivers will be available in the future.

Note: database-based drivers will require migrations to be run before using them. See the documentation for your chosen driver for more information.

Users

A ‘user’ in Pevensie is someone with a user ID. Users can be identified by ID, and optionally by email or phone number.

The User type contains some metadata about a user, such as their role, email, and password hash. This metadata is used by Pevensie to store information about users, such as their last sign in time, and their role in the system.

Note: this module only contains the User type and adjecent types and functions. If you wish to interact with users, you should use the pevensie/auth module.

Custom User Metadata

Custom user metadata can be added to the User type using the user_metadata field. This field is best used for storing data specific to your application, such as user preferences or usernames. This is done to reduce the chance you’ll need a separate database table/schema for storing your user data.

Different drivers may handle user metadata differently, though the general recommendation is to ensure your user metadata can be encoded to JSON (even if it’s not a direct mapping).

For example, if you have a user metadata type that contains a date:

pub type UserMetadata {
  UserMetadata(birthday: birl.Time)
}

You can encode this to JSON using the user_metadata_encoder function:

pub fn user_metadata_encoder(user_metadata: UserMetadata) -> json.Json {
  json.object([
    #("birthday", json.string(birl.to_iso8601(user_metadata.birthday))),
  ])
}

Your decoder can then handle decoding the JSON to a Time type, despite the fact that the JSON doesn’t support dates.

Session Management

Pevensie Auth provides a simple API for managing sessions. Sessions are managed using opaque session IDs, which are tied to a user ID and optionally an IP address and user agent.

Sessions are created using the create_session function, and retrieved using the get_session function. Sessions can be deleted using the delete_session function. Expired sessions may be deleted automatically by the driver, or they may be deleted manually using the delete_session function.

import gleam/option.{None, Some}
import pevensie/auth.{type PevensieAuth}
import pevensie/net

pub fn main() {
  // ...
  let session = auth.create_session(
    pevensie_auth,
    user.id,
    Some(net.parse_ip_address("127.1")),
    None,
    Some(24 * 60 * 60),
  )
  // ...
}

Cookies

Session tokens should be provided as cookies, and Pevensie Auth provides convenience functions for signing and verifying cookies. You can choose not to sign cookies, but it’s generally recommended to do so.

Types

Internal metadata used by Pevensie. Contains user information such as OAuth tokens, etc.

pub opaque type AppMetadata
pub type AuthDriver(driver, driver_error, user_metadata) {
  AuthDriver(
    driver: driver,
    connect: ConnectFunction(driver, driver_error),
    disconnect: DisconnectFunction(driver, driver_error),
    list_users: ListUsersFunction(
      driver,
      driver_error,
      user_metadata,
    ),
    create_user: CreateUserFunction(
      driver,
      driver_error,
      user_metadata,
    ),
    update_user: UpdateUserFunction(
      driver,
      driver_error,
      user_metadata,
    ),
    delete_user: DeleteUserFunction(
      driver,
      driver_error,
      user_metadata,
    ),
    get_session: GetSessionFunction(driver, driver_error),
    create_session: CreateSessionFunction(driver, driver_error),
    delete_session: DeleteSessionFunction(driver, driver_error),
    create_one_time_token: CreateOneTimeTokenFunction(
      driver,
      driver_error,
    ),
    validate_one_time_token: ValidateOneTimeTokenFunction(
      driver,
      driver_error,
    ),
    use_one_time_token: UseOneTimeTokenFunction(
      driver,
      driver_error,
    ),
    delete_one_time_token: DeleteOneTimeTokenFunction(
      driver,
      driver_error,
    ),
  )
}

Constructors

  • AuthDriver(
      driver: driver,
      connect: ConnectFunction(driver, driver_error),
      disconnect: DisconnectFunction(driver, driver_error),
      list_users: ListUsersFunction(
        driver,
        driver_error,
        user_metadata,
      ),
      create_user: CreateUserFunction(
        driver,
        driver_error,
        user_metadata,
      ),
      update_user: UpdateUserFunction(
        driver,
        driver_error,
        user_metadata,
      ),
      delete_user: DeleteUserFunction(
        driver,
        driver_error,
        user_metadata,
      ),
      get_session: GetSessionFunction(driver, driver_error),
      create_session: CreateSessionFunction(driver, driver_error),
      delete_session: DeleteSessionFunction(driver, driver_error),
      create_one_time_token: CreateOneTimeTokenFunction(
        driver,
        driver_error,
      ),
      validate_one_time_token: ValidateOneTimeTokenFunction(
        driver,
        driver_error,
      ),
      use_one_time_token: UseOneTimeTokenFunction(
        driver,
        driver_error,
      ),
      delete_one_time_token: DeleteOneTimeTokenFunction(
        driver,
        driver_error,
      ),
    )
pub type CreateError(auth_driver_error) {
  CreateDriverError(auth_driver_error)
  CreatedTooFewRecords
  CreatedTooManyRecords
  CreateHashError(argus.HashError)
  CreateInternalError(String)
}

Constructors

  • CreateDriverError(auth_driver_error)
  • CreatedTooFewRecords
  • CreatedTooManyRecords
  • CreateHashError(argus.HashError)
  • CreateInternalError(String)
pub type DeleteError(auth_driver_error) {
  DeleteDriverError(auth_driver_error)
  DeletedTooFewRecords
  DeletedTooManyRecords
  DeleteInternalError(String)
}

Constructors

  • DeleteDriverError(auth_driver_error)
  • DeletedTooFewRecords
  • DeletedTooManyRecords
  • DeleteInternalError(String)
pub type GetError(auth_driver_error) {
  GetDriverError(auth_driver_error)
  GotTooFewRecords
  GotTooManyRecords
  GetInternalError(String)
}

Constructors

  • GetDriverError(auth_driver_error)
  • GotTooFewRecords
  • GotTooManyRecords
  • GetInternalError(String)
pub type LogInError(auth_driver_error) {
  LogInUserError(GetError(auth_driver_error))
  LogInSessionError(CreateError(auth_driver_error))
}

Constructors

  • LogInUserError(GetError(auth_driver_error))
  • LogInSessionError(CreateError(auth_driver_error))
pub type OneTimeTokenType {
  PasswordReset
}

Constructors

  • PasswordReset

The entrypoint to the Pevensie Auth API. This type is used when using the majority of the functions in pevensie/auth.

You must connect your Pevensie Auth instance before using it. This allows your driver to perform any setup required, such as creating a database connection pool.

Create a new PevensieAuth instance using the new function.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  let pevensie_auth = auth.new(
    postgres.new_auth_driver(postgres.default_config()),
    user_metadata_decoder,
    user_metadata_encoder,
    "super secret signing key",
  )
  // ...
}
pub opaque type PevensieAuth(
  driver,
  driver_error,
  user_metadata,
  connected,
)

A Pevensie session. Sessions are used to identify users and provide a way to track their activity.

Fields:

  • id: The session’s ID (unique).
  • created_at: The time the session was created.
  • expires_at: The time the session will expire.
  • user_id: The ID of the user associated with the session.
  • ip: The IP address of the user associated with the session.
  • user_agent: The user agent of the user associated with the session.

When searching for sessions, auth drivers will check for IP and user agent matches if the ip and user_agent fields are not None.

pub type Session {
  Session(
    id: String,
    created_at: Time,
    expires_at: Option(Time),
    user_id: String,
    ip: Option(IpAddress),
    user_agent: Option(String),
  )
}

Constructors

  • Session(
      id: String,
      created_at: Time,
      expires_at: Option(Time),
      user_id: String,
      ip: Option(IpAddress),
      user_agent: Option(String),
    )
pub type UpdateError(auth_driver_error) {
  UpdateDriverError(auth_driver_error)
  UpdatedTooFewRecords
  UpdatedTooManyRecords
  UpdateHashError(argus.HashError)
  UpdateInternalError(String)
}

Constructors

  • UpdateDriverError(auth_driver_error)
  • UpdatedTooFewRecords
  • UpdatedTooManyRecords
  • UpdateHashError(argus.HashError)
  • UpdateInternalError(String)

A field to be updated in the database. Use the Set constructor to set a field to a value, and the Ignore constructor to leave the field unchanged.

pub type UpdateField(a) {
  Set(a)
  Ignore
}

Constructors

  • Set(a)
  • Ignore

Pevensie’s user type. Users can be identified by ID, email, or phone number - all of which are unique.

Fields:

  • id: The user’s ID (unique).
  • created_at: The time the user was created.
  • updated_at: The time the user was last updated.
  • deleted_at: The time the user was deleted.
  • role: The user’s role. This can be set to any string, and is not used by Pevensie.
  • email: The user’s email address.
  • password_hash: The user’s hashed password.
  • app_metadata: Pevensie’s internal metadata about the user.
  • user_metadata: Custom user metadata, such as username, avatar, etc. This is not used by Pevensie.
  • last_sign_in: The time the user last signed in.

The user_metadata field is a generic field that can be used to store any custom data about the user. When creating a PevensieAuth instance, you will need to provide a user_metadata decoder and encoder.

A few fields currently unused, but will be used in the future:

  • email_confirmed_at: The time the user confirmed their email address.
  • phone_number_confirmed_at: The time the user confirmed their phone number.
  • banned_until: The time the user will be banned.
pub type User(user_metadata) {
  User(
    id: String,
    created_at: Time,
    updated_at: Time,
    deleted_at: Option(Time),
    role: Option(String),
    email: String,
    password_hash: Option(String),
    email_confirmed_at: Option(Time),
    phone_number: Option(String),
    phone_number_confirmed_at: Option(Time),
    last_sign_in: Option(Time),
    app_metadata: AppMetadata,
    user_metadata: user_metadata,
    banned_until: Option(Time),
  )
}

Constructors

  • User(
      id: String,
      created_at: Time,
      updated_at: Time,
      deleted_at: Option(Time),
      role: Option(String),
      email: String,
      password_hash: Option(String),
      email_confirmed_at: Option(Time),
      phone_number: Option(String),
      phone_number_confirmed_at: Option(Time),
      last_sign_in: Option(Time),
      app_metadata: AppMetadata,
      user_metadata: user_metadata,
      banned_until: Option(Time),
    )

A user to be inserted into the database. See User for fields.

pub type UserCreate(user_metadata) {
  UserCreate(
    role: Option(String),
    email: String,
    password_hash: Option(String),
    email_confirmed_at: Option(Time),
    phone_number: Option(String),
    phone_number_confirmed_at: Option(Time),
    last_sign_in: Option(Time),
    app_metadata: AppMetadata,
    user_metadata: user_metadata,
  )
}

Constructors

  • UserCreate(
      role: Option(String),
      email: String,
      password_hash: Option(String),
      email_confirmed_at: Option(Time),
      phone_number: Option(String),
      phone_number_confirmed_at: Option(Time),
      last_sign_in: Option(Time),
      app_metadata: AppMetadata,
      user_metadata: user_metadata,
    )

A set of fields to use when searching for or listing users. Each field can be set to a list of values to search for, to search for multiple values at once.

Different drivers may handle search fields differently. See the documentation for the specific driver for more information.

Can be constructed more easily using default_user_search_fields.

pub type UserSearchFields {
  UserSearchFields(
    id: Option(List(String)),
    email: Option(List(String)),
    phone_number: Option(List(String)),
  )
}

Constructors

  • UserSearchFields(
      id: Option(List(String)),
      email: Option(List(String)),
      phone_number: Option(List(String)),
    )

A user to be updated in the database. See User for fields.

Can be constructed more easily using default_user_update.

pub type UserUpdate(user_metadata) {
  UserUpdate(
    role: UpdateField(Option(String)),
    email: UpdateField(String),
    password_hash: UpdateField(Option(String)),
    email_confirmed_at: UpdateField(Option(Time)),
    phone_number: UpdateField(Option(String)),
    phone_number_confirmed_at: UpdateField(Option(Time)),
    last_sign_in: UpdateField(Option(Time)),
    app_metadata: UpdateField(AppMetadata),
    user_metadata: UpdateField(user_metadata),
  )
}

Constructors

  • UserUpdate(
      role: UpdateField(Option(String)),
      email: UpdateField(String),
      password_hash: UpdateField(Option(String)),
      email_confirmed_at: UpdateField(Option(Time)),
      phone_number: UpdateField(Option(String)),
      phone_number_confirmed_at: UpdateField(Option(Time)),
      last_sign_in: UpdateField(Option(Time)),
      app_metadata: UpdateField(AppMetadata),
      user_metadata: UpdateField(user_metadata),
    )

Functions

pub fn app_metadata_encoder(app_metadata: AppMetadata) -> Json

Encodes an AppMetadata value to JSON.

pub fn connect(
  pevensie_auth: PevensieAuth(a, b, c, Disconnected),
) -> Result(PevensieAuth(a, b, c, Connected), ConnectError(b))

Runs setup for your chosen auth driver and returns a connected PevensieAuth instance.

This function must be called before using any other functions in the Pevensie Auth API. Attempting to use the API before calling connect will result in a compile error.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  // ...
  let assert Ok(pevensie_auth) = auth.connect(pevensie_auth)
  // ...
}
pub fn create_one_time_token(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  token_type token_type: OneTimeTokenType,
  ttl_seconds ttl_seconds: Int,
) -> Result(String, CreateError(b))
pub fn create_session(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  ip ip: Option(IpAddress),
  user_agent user_agent: Option(String),
  ttl_seconds ttl_seconds: Option(Int),
) -> Result(Session, CreateError(b))

Create a new session for a user. If IP address or user agent are provided, they will be stored alongside the session, and must be provided when fetching the session later.

You can optionally set a TTL for the session, which will cause the session to expire. Set ttl_seconds to None to never expire the session.

You can optionally delete any other active sessions for the user. This may be useful if you want to ensure that a user can only have one active session at a time.

pub fn create_session_cookie(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  session session: Session,
) -> Result(String, Nil)

Create a signed cookie for a session.

pub fn create_user_with_email(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  email email: String,
  password password: String,
  user_metadata user_metadata: c,
) -> Result(User(c), CreateError(b))

Create a new user with the given email and password.

pub fn default_user_search_fields() -> UserSearchFields

A convenience function to create a UserSearchFields with all fields set to None.

import pevensie/user

fn main() {
  // ...
  user.list_users(
    pevensie_auth,
    user.UserSearchFields(
      ..user.default_user_search_fields(),
      email: Some(["isaac@pevensie.dev"]),
    ),
  )
}
pub fn default_user_update() -> UserUpdate(a)

A convenience function to create a UserUpdate with all fields set to Ignore.

import pevensie/auth
import pevensie/user.{Set}

fn main() {
  // ...
  auth.update_user(
    pevensie_auth,
    user.id,
    user.UserUpdate(
      ..user.default_user_update(),
      email: Set("new_email@example.com"),
    ),
  )
}
pub fn delete_one_time_token(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  token_type token_type: OneTimeTokenType,
  token token: String,
) -> Result(Nil, DeleteError(b))
pub fn delete_session(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  session_id session_id: String,
) -> Result(Nil, DeleteError(b))

Delete a session by ID.

pub fn disconnect(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
) -> Result(
  PevensieAuth(a, b, c, Disconnected),
  DisconnectError(b),
)

Runs teardown for your chosen auth driver and returns a disconnected PevensieAuth instance.

pub fn get_session(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  session_id session_id: String,
  ip ip: Option(IpAddress),
  user_agent user_agent: Option(String),
) -> Result(Session, GetError(b))

Fetch a session by ID. If IP address or user agent were provided when the session was created, they should be provided here as well.

pub fn get_user_by_email(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  email email: String,
) -> Result(User(c), GetError(b))

Fetch a single user by email.

Errors if exactly one user is not found.

pub fn get_user_by_email_and_password(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  email email: String,
  password password: String,
) -> Result(User(c), GetError(b))

Fetch a single user by email and password.

Errors if exactly one user is not found, or if the password for the user is incorrect.

pub fn get_user_by_id(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
) -> Result(User(c), GetError(b))

Fetch a single user by ID.

Errors if exactly one user is not found.

pub fn list_users(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  limit limit: Int,
  offset offset: Int,
  filters filters: UserSearchFields,
) -> Result(List(User(c)), GetError(b))

Retrieves a list of users based on the given search fields.

The filters argument is a UserSearchFields type that contains the search fields to use. The UserSearchFields type contains a number of fields, such as id, email, and phone_number. Each field can be set to a list of values to search for, or to None to search for all values.

Drivers may handle search fields differently, so see the documentation for your chosen driver for more information.

pub fn log_in_user(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  email email: String,
  password password: String,
  ip ip: Option(IpAddress),
  user_agent user_agent: Option(String),
) -> Result(#(Session, User(c)), LogInError(b))

Log in a user using email and password.

Assuming credentials are valid, this function will create a new session for the user, and update the user’s last sign in time to the current UTC time.

Uses a session TTL of 24 hours, and does not delete any other sessions for the user.

pub fn new(
  driver driver: AuthDriver(a, b, c),
  user_metadata_decoder user_metadata_decoder: fn(Dynamic) ->
    Result(c, List(DecodeError)),
  user_metadata_encoder user_metadata_encoder: fn(c) -> Json,
  cookie_key cookie_key: String,
) -> PevensieAuth(a, b, c, Disconnected)

Creates a new PevensieAuth instance.

The driver argument is the driver to use for authentication. This should be the driver that you’ve created using the new_auth_driver function.

The user_metadata_decoder and user_metadata_encoder arguments are used to decode and encode user metadata. These should be the inverse of each other, and should be able to handle both decoding and encoding user metadata to JSON.

The cookie_key argument is used to sign and verify cookies. It should be a long, random string. It’s recommended to use a secret key from a cryptographically secure source, and store it in a secure location.

pub fn set_user_email(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  email email: String,
) -> Result(User(c), UpdateError(b))

Update a user’s email.

pub fn set_user_password(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  password password: Option(String),
) -> Result(User(c), UpdateError(b))

Update a user’s password.

If a password is provided, it will be hashed using Argon2 before being stored in the database. If no password is provided, the user’s password will be set to null.

pub fn set_user_role(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  role role: Option(String),
) -> Result(User(c), UpdateError(b))

Update a user’s role.

pub fn update_last_sign_in(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  last_sign_in last_sign_in: Option(Time),
) -> Result(User(c), UpdateError(b))

Update a user’s last sign in time.

pub fn update_user(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  user_update user_update: UserUpdate(c),
) -> Result(User(c), UpdateError(b))

Update a user by ID. See UserUpdate for more information on how to provide the fields to be updated.

import pevensie/auth.{type PevensieAuth}

pub fn main() {
  // ...
  let assert Ok(user) = auth.update_user(
    pevensie_auth,
    user.id,
    UserUpdate(..user.default_user_update(), email: Set("new_email@example.com")),
  )
  // ...
}

Note: if updating the user’s password, you should use the set_user_password function instead in order to hash the password before storing it in the database.

pub fn update_user_metadata(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  user_metadata user_metadata: c,
) -> Result(User(c), UpdateError(b))

Update user metadata.

pub fn use_one_time_token(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  token_type token_type: OneTimeTokenType,
  token token: String,
) -> Result(Nil, UpdateError(b))
pub fn user_encoder(
  user: User(a),
  user_metadata_encoder: fn(a) -> Json,
) -> Json

Encodes a User value to JSON.

pub fn validate_one_time_token(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  user_id user_id: String,
  token_type token_type: OneTimeTokenType,
  token token: String,
) -> Result(Nil, GetError(b))
pub fn validate_session_cookie(
  pevensie_auth: PevensieAuth(a, b, c, Connected),
  cookie cookie: String,
) -> Result(String, Nil)

Validate a signed cookie for a session. Returns the session ID if the cookie is valid.

Search Document