Sqlite.Auth.Password
Create, hash, store, validate, change, reset, and delete users' passwords.
This module prevents saving passwords as plain-text strings through its
functions. The Password type enforces this at compile time.
Passwords must be hashed before they can be saved to the database, and
all verification functions properly compare against the stored password hash.
Migrations
All migrations are meant to be plug-and-play with the gren-sqlite-migrate
package:
Sqlite.Migrate.migrate database
{ secureContext = secureContext }
(Sqlite.Auth.Password.migrationV1 ++ yourOtherMigrations)
If you are using a different method of migration, the individual SQL statements
can be extracted from each migration's statements field.
Initial migration required for functions in this module to operate. Adds
the passwords and password_reset_tokens tables. Both reference users, so
run this after Sqlite.Auth.User.migrationV1.
The first statement creates the passwords table:
CREATE TABLE IF NOT EXISTS passwords (
user_id TEXT PRIMARY KEY NOT NULL,
password TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_used INTEGER,
failed_login_attempts_count INTEGER NOT NULL CHECK (failed_login_attempts_count >= 0),
locked_until INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
user_idis the primary key and references the users (in theSqlite.Auth.Usermodule), so there is one password per user.passwordis the user's hashed password, stored in PHC string format.- Both
created_atandupdated_atare posix timestamp values. last_usedis a posix timestamp of the last successful check, or null if never.failed_login_attempts_countcounts consecutive failed checks.locked_untilis the posix timestamp the password stays locked until after too many failures, or null when unlocked.
The second statement creates the password_reset_tokens table:
CREATE TABLE IF NOT EXISTS password_reset_tokens (
reset_token TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
reset_tokenis the primary key and stores the SHA-256 hash of the token.user_idreferences the users and isUNIQUE, so there is at most one live reset token per user.created_atis a posix timestamp, andexpires_atis the posix timestamp the token stops being redeemable.
Working with passwords
A password, with a phantom type state of either PlainText or
Hashed. Only the hashed form can be saved to the database.
A password in plain text, as received from the user.
A password that has been hashed and is safe to persist.
Creating passwords
The options the password functions take.
iterationsis the PBKDF2 round count for new hashes. TheCryptolibrary enforces a minimum of600000for SHA-256. Lower values makehashfail with aHashError. Values ten times the default (6000000 or above) are also denied aserrormaxFailedAttemptsandlockoutDurationMilliscontrol the lockout.resetTokenLifetimeMillisis how long a reset token stays redeemable. Longer lifetimes forgive users who click late; shorter ones shrink the window a leaked link stays live. SeerequestReset.
The default, recommended, options for password hashing.
{ iterations = 600000
, maxFailedAttempts = 5
, lockoutDurationMillis = 900000 -- 15 minutes
, resetTokenLifetimeMillis = 1800000 -- 30 minutes
}
Turn a String into a plain-text Password, ready to
hash, or to pass to validate.
Hash a plain-text password into a Password Hashed ready for
add. Uses PBKDF2-SHA256 with options.iterations rounds and a random
16-byte salt. Returns a Password Hashed that can then be saved to the database.
Iterations above ten times the default are rejected with
TooManyIterations, so the cost of every stored hash
stays bounded.
Note that the algorithm used to produce the password hash is SHA-256. This cannot currently be changed.
Errors that can happen while deriving a password hash.
ImportPbkdf2KeyErrorhappens when the plain-text password cannot be imported as PBKDF2 key material.Pbkdf2DeriveBitsErrorhappens when the PBKDF2 derivation itself fails.TooManyIterationshappens raised whenhashis asked for more rounds than ten times the default. It carries the rejected count.
Adding and validating passwords
Store a hashed password for the given user to the database, returning the user on success for further pipeline steps.
User.new database secureContext
|> Task.andThen (Username.new database alice)
|> Task.andThen (Password.add database hashedPassword)
Check a plain-text password against a user's stored password. The user argument
is the Maybe that Sqlite.Auth.Username.user returns
to make chaining easier. For example, you might handle user login like this:
Username.user database givenUserName
|> Task.andThen (Password.validate database secureContext options candidate)
|> Task.andThen
(\outcome ->
when outcome is
Password.Verified proof ->
Session.issue database secureContext sessionOptions proof
...
)
The outcome of validating a candidate password.
Verifiedcontains the verified userSqlite.Auth.Session.issuerequires to issue a session.Rejectedmeans the candidate did not match or the user has no password.Lockedmeans the password is temporarily locked after repeated failures.
An error when validating a password.
StorageErroris an underlying error in the Sqlite database.VerificationErroris a fault in the stored hash itself; seeVerifyErrorfor the ways it can happen.
The ways a stored PHC string can fail to verify against other than a plain mismatch.
ParsePhcStringErroris when the stored hash is not a valid PHC string.UnknownDigestAlgorithmis when the PHC string names a digest algorithm this module does not support.MissingSaltis when the PHC string has no salt segment.InvalidSaltis when the salt segment is not valid base64.SaltTooLongis when the salt exceeds the accepted maximum length.MissingHashis when the PHC string has no hash segment.InvalidHashis when the hash segment is not valid base64.HashLengthMismatchis when the hash length does not match the digest algorithm's native output size.MissingIterationsis when the PHC string has noi(iterations) parameter.InvalidIterationsis when theiparameter is not a valid integer.IterationsTooLargeis when the iteration count exceeds the accepted maximum.RederiveErroris when re-deriving the candidate's hash fails; it wraps aHashError.
Changing, resetting, and deleting passwords
Replace a user's password. This requires the Verified type. A password
check with validate creates it, and so does verifying a live session
through Sqlite.Auth.Session.verify. To
require the current password, check it with validate and pass along
the Verified that check returns.
Changing the password revokes the user's sessions and deletes any outstanding reset token.
All operations are completed in a SQLite transaction, preventing data corruption.
Remove a user's password and any outstanding reset token, leaving the
account without a password. Requires the Verified type,
which a password check with validate or a live session through
Sqlite.Auth.Session.verify creates.
Existing sessions are not revoked. They are a separate credential and keep working until they expire. Deleting a password when there is none succeeds and changes nothing. Both writes happen in one transaction.
Creates a reset token for the given user and returns { token } exactly once.
It's intended for this token to be transmitted safely, such as being emailed to the
user.
The token is a standard base64 string, so it can contain +, /, and =. This
library does not make the token safe for any particular transport: if it travels
in a URL or form field, encode it for that context and hand reset
back the exact original string.
A new request replaces any existing token for the same user, so the previous link stops working immediately.
To reset the password using the generated token, use the reset function.
Use a generated reset token to set a new password. No user is needed, because the token resolves the user itself.
If successful, this function deletes the token, replaces the password, and revokes
all sessions the user may have. This function returns Verified,
allowing you to issue a new session if you choose after a successful reset.
Note: A token can be redeemed only once and is invalid after redemption.
Password.reset database secureContext
{ token = generatedToken
, new = hashedNew -- `Password Hashed`
}
|> Task.andThen (Session.issue database secureContext sessionOptions)
The ways redeeming a reset token can go wrong.
InvalidResetTokenis a token that matches no live row, whether it is unknown, already used, replaced, or expired. The caller cannot tell which, deliberately.ResetStorageErroris a database fault.