BoxLang 🚀 A New JVM Dynamic Language Learn More...
RememberMe is a Coldbox module designed to work in conjunction with your authentication system to "remember" and automatically log in users on subsequent website visits.
Within Commandbox type:
box install rememberMe
Copy over the configuration object below into your /config/Coldbox.cfc
moduleSettings section.
rememberMe = {
userServiceClass = "",
tokenEncryptKey = "",
days = 30,
autoPurge = true, // daily scheduled purge of stale token rows (see "Automatic purging")
purgeGraceDays = 1, // keep rows this many days past expiration; 0 = purge immediately on expiry
purgeTime = "04:00", // daily purge run time, 24h server time
table = "user_remember", // token table used by the default storage
datasource = "", // "" = your app's default datasource (this.datasource in Application.cfc)
tokenStorageClass = "QBTokenStorage@rememberMe" // swap in your own storage (see "Custom token storage")
}
You will need to specify a userServiceClass that
implements the method retrieveUserById(). You will also
need to generate a unique encryption key that will be used when
generating tokens. Hint: You can generate a valid random key by
executing the following code generateSecretKey("AES", 256).
Make sure your CFML datasource has a database table with the
following columns (currently tested with MSSQL Server). The table name
defaults to user_remember and is configurable via the
table setting; the datasource setting lets
you keep token rows in a different datasource entirely (empty means
your application default): | column name | type |
|-----------------|----------| | id | int | |
createdDate | datetime | | modifiedDate | datetime | | userId
| int | | selector | varchar(35)| | hashedValidator |
varchar(32)| | ipAddress | varchar(45)| | userAgent |
varchar(255)| | expirationDate | datetime | | lastUsedDate |
datetime |
RememberMe automatically injects a remember() helper
into all Coldbox interceptors. Here's an example of how you might
utilize RememberMe on the Coldbox preProcess()
interceptor method on an app that uses cbauth for their authentication provider:
function preProcess( event, interceptData, buffer, rc, prc ) {
// if the user is not logged in, and the rememberMe cookie exists, attempt to recall the user
if (
!auth().isLoggedIn() && // <-- cbAuth method
remember().cookieExists()
) {
try {
// attempt to recall the user
// if successful, returns a user object from your `userServiceClass`
var user = remember().recallMe();
// verify the user exists and log them in using cbauth
if ( user.isLoaded() ) {
auth().login( user ); // <-- cbAuth method
}
// if the token is invalid, forget the user and cleanup bad cookies
} catch( InvalidToken e ) {
remember().forgetMe();
}
}
Expired tokens are already unusable — recallMe() rejects
them — but their rows would otherwise sit in the table forever. The
module registers a ColdBox scheduled task
(rememberMe-purge-expired-tokens) that runs daily at
purgeTime and deletes rows whose
expirationDate passed more than
purgeGraceDays days ago. The grace period keeps
recently-expired rows around briefly in case you want them for auditing.
autoPurge =
false in your module settings to disable it; the task stays
registered but does nothing.getInstance( "RememberMeService@rememberMe" ).purgeExpired(); // uses purgeGraceDays
getInstance( "RememberMeService@rememberMe" ).purgeExpired( 0 ); // purge everything already expired
purgeExpired() returns the number of rows deleted.
purgeTime is interpreted in the server's timezone.By default the module persists tokens itself with qb
(models/QBTokenStorage.cfc), against the
table and datasource settings above. If that
doesn't fit — you want your ORM, a separate token store, Redis,
anything — point tokenStorageClass at any
WireBox-resolvable class of your own:
rememberMe = {
...
tokenStorageClass = "TokenStorage" // resolved via WireBox, like userServiceClass
}
Your class must satisfy the contract in
interfaces/ITokenStorage.cfc (the shipped
models/QBTokenStorage.cfc is the reference implementation):
| method | arguments | returns |
|---|---|---|
create
| token struct: userId,
selector, hashedValidator,
ipAddress, userAgent,
createdDate, modifiedDate, expirationDate
| — |
getBySelector
| selector
| struct with at least userId,
selector, hashedValidator,
expirationDate — empty struct when
not found, never null |
updateUsage
| selector, audit struct:
ipAddress, userAgent,
lastUsedDate, modifiedDate
| — |
deleteBySelector
| selector
| — |
deleteByUserId
| userId
| — |
deleteAll
| — | — |
deleteExpiredBefore
| cutoffDate
| number of rows deleted (0 if unknown) |
Two guarantees make implementations simple and safe:
Sometimes the first load of an app will throw an error stating that
remember cannot be found. I believe this has to do with
a "chicken and egg" problem where sometimes every Coldbox
dependency is loaded when the first onSessionStart()
method executes. I recommend using preProcess() instead
of onSessionStart() to avoid this issue for now.
This is a custom interception point that fires when the
remember().recall() method is called. You can use this to
add custom logic, such as logging or additional processing, during the
recall process.
| Name | Description |
|---|---|
| user | The user object returned by the
remember().recall() method. |
| userId | The ID of the user returned by the
remember().recall() method. |
tokenStorageClass setting (a WireBox DSL, mirroring userServiceClass). The default, QBTokenStorage@rememberMe (models/QBTokenStorage.cfc), is the same qb code as before — public API and out-of-the-box behaviour are unchanged. The contract lives in interfaces/ITokenStorage.cfc; providers receive plain values only, and never see a raw validator (all crypto stays in the service).tokenStorageClass (default "QBTokenStorage@rememberMe"), table (default "user_remember" — the previously hardcoded table name), and datasource (default "" = the application default from your Application.cfc, passed per-query via qb's options).QBTokenStorageSpec.cfc, new integration bundle CustomStorageSpec.cfc (full lifecycle against an in-memory provider, plus datasource-option plumbing), and a harness StubTokenStorage.cfc that implements the shipped interface to prove it is satisfiable.config/Scheduler.cfc, registered as cbScheduler@rememberMe) runs daily at purgeTime and deletes rows whose expirationDate passed more than purgeGraceDays days ago. Enabled by default; set autoPurge = false to disable (the task stays registered but no-ops). Expired rows were already unusable — recallMe() rejects them — this is table hygiene.purgeExpired( numeric graceDays ) returning the number of rows deleted, for manual/host-app-scheduled cleanup.autoPurge (default true), purgeGraceDays (default 1), purgeTime (default "04:00", server time).IX_user_remember_expirationDate in the canonical schema (test-harness/tests/resources/schema.sql), added idempotently for existing databases.PurgeSpec.cfc plus ModuleSpec assertions for the scheduler, task, and settings defaults.beforeAll(). All bundles in a runner request share one request, and ColdBox 7's WireBox memoises transient dependencies there (request.cbTransientDICache) — so restarting mid-request left later bundles' rebuilt transients wired to the previous boot's shut-down services. The visible symptom was onRecall announcements that no registered interceptor ever heard, in multi-bundle runs only. Latent until 1.3.0 added a second integration bundle. See AGENTS.md trap 6.The suite is now green on all four engines (Lucee 5, Lucee 6, Adobe 2023, BoxLang 1). The 1.2.0 "Known issues" entry below is resolved:
rememberMe() is now a portable cfcookie() call with a DateTime expires instead of a Lucee-only attribute-struct assignment to the cookie scope. This fixes every rememberMe() call erroring on BoxLang (Can't cast [30] to a DateTime). path="/" is set on all engines except Adobe, whose cfcookie refuses path without domain — ACF defaults its cookies to Path=/ anyway.cookieExists() now treats an empty cookie value as absent. Adobe CF never removes an expired/deleted cookie's key from the in-request cookie scope — it leaves it behind with an empty value — so after forgetMe(), recallMe() on ACF threw InvalidToken where it should throw MissingCookie. An empty token is unusable regardless of engine, so "empty means missing" is the honest semantic everywhere.forgetMe() uses structDelete() instead of the member-function form cookie.delete().Fixed: the validator half of the selector/validator scheme was dead code. Two bugs cancelled each other out, so nothing looked broken:
parseToken() re-hashed an already-hashed value, so the parsed validator could never equal the stored one.isMatch() was inverted — compare() returns 0 when strings are equal, so the function returned true when they differed.The net effect was that the validator comparison in recallMe() never rejected anything. Any decryptable cookie whose selector matched a database row would authenticate, regardless of its validator. The encryption key was the only real secret.
rememberMe() now stores the hashed validator in the database and puts the raw validator in the cookie — the canonical scheme, where a stolen database yields hashes an attacker cannot present back. isMatch() compares correctly.
Breaking: existing remember-me cookies will no longer validate. They are rejected as InvalidToken, which the documented consumer pattern already catches and handles by calling forgetMe(). Users will be logged out once on deploy.
rememberMe() did not populate modifiedDate on INSERT, but the documented schema has that column as NOT NULL with no default — so the module could not write a row to its own schema. It now sets modifiedDate at creation.test-harness/ with unit and integration suites (46 specs). See AGENTS.md for how to run them, and for the per-engine status matrix.qb is now declared as a dependency in box.json. ModuleConfig.cfc has always declared this.dependencies = [ "qb" ], but box install rememberMe never actually installed it.The cookie write in rememberMe() assigns a struct of cookie attributes to the cookie scope, which is Lucee-specific. The suite is green on Lucee 5 and 6, and fails on Adobe 2023 (4 specs) and BoxLang (16 specs) because of it. See AGENTS.md for detail.
Version bump.
Added custom interception point, onRecall, to interceptor settings in the module configuration. This interceptor fires when the remember().recall() method is called, allowing for custom logic to be executed during the recall process (like logging).
Initial release.
Changed the method name for retrieving users to match the interface used by cbauth. We will now use retrieveUserById() instead of getUserById().
$
box install rememberMe