Sqlite.Auth.Session

Issue, verify, and revoke users' login sessions.

A session is a login token tied to a user. The raw token is handed to the client once, when the session is issued. Only its SHA-256 hash is stored, so a stolen database cannot be replayed as live sessions.

Migrations

All migrations are meant to be plug-and-play with the gren-sqlite-migrate package:

Sqlite.Migrate.migrate database
    { secureContext = secureContext }
    (Sqlite.Auth.Session.migrationV1 ++ yourOtherMigrations)

If you are using a different method of migration, the individual SQL statements can be extracted from each migration's statements field.

migrationV1 : { name : String, statements : Array String }

Initial migration required for functions in this module to operate.

CREATE TABLE IF NOT EXISTS sessions (
    session_token TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL,
    last_used_at INTEGER NOT NULL,
    revoked_at INTEGER,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
  • session_token is the primary key and stores the SHA-256 hash of the token.
  • user_id references the user's id.
  • created_at is a posix timestamp, and expires_at is the posix timestamp the session stops being valid.
  • last_used_at is a posix timestamp of the last successful verification, used to enforce the idle timeout.
  • revoked_at is the posix timestamp the session was revoked, or null while live.

Working with sessions

type alias Session =
{ sessionToken : String
, userId : String
, createdAt : Int
, expiresAt : Int
, lastUsedAt : Int
, revokedAt : Maybe Int
}

A row from the sessions table.

type alias Options = { lifetimeMillis : Int, idleTimeoutMillis : Int }

Options for sessions. lifetimeMillis is the maximum length the token is valid for before expiring. idleTimeoutMillis is the maximum length the token can exist without being used before expiring.

defaultOptions : Options

The default, recommended, options for sessions.

{ lifetimeMillis = 30 * 24 * 60 * 60 * 1000 -- 30 days
, idleTimeoutMillis = 7 * 24 * 60 * 60 * 1000 -- 7 days
}
issue :
Database
-> SecureContext
-> Options
-> Verified
-> Task Error { session : Session, token : String
}

Issue a session for a Verified user. You can obtain a Verified with a credential check such as what Sqlite.Auth.Password.validate produces.

This function returns two things: the session and the token.

The session is the row saved to the database representing the session. This includes the hashed session token. This value is returned to be inspected, never to be sent to the client.

The token value is the raw token (not hashed). This value should be sent safely to the user's browser. It's highly recommended you send the token to the client as a cookie that is HttpOnly, Secure, and has SameSite=Lax rules. Without these rules, or if the raw token is saved via IndexedDB, localStorage, or other methods, it's vulnerable to being stolen by attacks.

verify :
Database
-> SecureContext
-> Options
-> { token : String }
-> Task VerifyError { session : Session, verified : Verified
}

Verify a presented token, succeeding with the Session and the user whose session has been verified. A revoked, expired, or idle session, or a token matching no session at all — fails the task with the matching VerifyError.

The returned, Verified, user lets a live session authorize actions without re-checking a credential:

Session.verify database secureContext options { token = token }
    |> Task.andThen .verified
    |> Task.andThen (Username.change database { from = old, to = new })

It's recommended session tokens are verified for each authenticated request, ensuring the user performing a specific action is able and allowed to do so.

type VerifyError
= StorageError Error
| TokenNotFound
| Revoked
| Expired
| IdleTimeout

Why a presented token failed to verify.

  • StorageError is an underlying error in the Sqlite database.
  • TokenNotFound is when the token matches no session row at all.
  • Revoked is when the session has been revoked.
  • Expired is when the session is past its absolute lifetime.
  • IdleTimeout is when the session went unused for longer than idleTimeoutMillis.
list :
Database
-> { userId : String }
-> Task Error (Array Session)

List all of a given user's live sessions. Live is defined as sessions that have not yet been revoked or are beyond their lifetimes.

revoke :
Database
-> SecureContext
-> Options
-> { token : String }
-> Task Error {}

Revoke a session by its raw token. If the given token does not exist, the Task will succeed, but no changes will be made to the database.

revokeAll : Database -> { userId : String } -> Task Error {}

Revoke every live session a user holds.