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:
postgres
- A driver for PostgreSQL
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 thepevensie/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.