BoxLang 🚀 A New JVM Dynamic Language Learn More...

Remember Me

v1.4.0 Modules

RememberMe

RememberMe icon

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.

Engine Support

  • Lucee 5+
  • Adobe ColdFusion 2023+
  • Boxlang 1+

Requirements

  • Coldbox 8+
  • Cbauth or your own authentication provider

Installation

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 |

Usage

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();
        }

    }

Automatic purging

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.

  • It is on by default. Set autoPurge = false in your module settings to disable it; the task stays registered but does nothing.
  • You can also purge manually (for example from your own scheduled task or a maintenance script):
getInstance( "RememberMeService@rememberMe" ).purgeExpired();     // uses purgeGraceDays
getInstance( "RememberMeService@rememberMe" ).purgeExpired( 0 );  // purge everything already expired

purgeExpired() returns the number of rows deleted.

  • Clustering note: the task runs on every node. That is deliberate — constraining it to one server requires a distributed CacheBox region, and a concurrent double-run of this DELETE is harmless (it is idempotent).
  • purgeTime is interpreted in the server's timezone.

Custom token storage

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:

  • Everything is a plain value (strings, numerics, native dates). The service computes all of it — dates included — so storage holds no policy.
  • Storage never sees a raw validator. All crypto happens in the service before storage is called; you only ever store the selector and the already-hashed validator, so a custom provider cannot weaken the token scheme.

Known Issues

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.

Intercetion Points

onRecall

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.

InterceptData

Name Description
userThe user object returned by the remember().recall() method.
userIdThe ID of the user returned by the remember().recall() method.

Future Development Roadmap

  • Get community feedback for improving the module and documentation.
  • Automatically create table in datasource if missing.

Change Log

1.4.0

Added

  • Pluggable token storage. Persistence is extracted behind a storage-provider seam: the service delegates all reads/writes to the class named by the new 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).
  • New module settings: 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).
  • New unit bundle 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.

1.3.0

Added

  • Automatic purging of stale token rows. A ColdBox scheduled task (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.
  • New public service method purgeExpired( numeric graceDays ) returning the number of rows deleted, for manual/host-app-scheduled cleanup.
  • New module settings: autoPurge (default true), purgeGraceDays (default 1), purgeTime (default "04:00", server time).
  • New index IX_user_remember_expirationDate in the canonical schema (test-harness/tests/resources/schema.sql), added idempotently for existing databases.
  • New integration bundle PurgeSpec.cfc plus ModuleSpec assertions for the scheduler, task, and settings defaults.

Fixed

  • Test harness: both base spec classes no longer restart the ColdBox virtual app in 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.

1.2.1

Fixed

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:

  • The cookie write in 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().

1.2.0

Security

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.

Fixed

  • 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.

Added

  • A TestBox 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.

Known issues (fixed in 1.2.1)

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.

1.1.1

Version bump.

1.1.0

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).

1.0.0

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

No collaborators yet.
     
  • {{ getFullDate("2021-09-14T20:28:57Z") }}
  • {{ getFullDate("2026-07-12T01:28:32Z") }}
  • 2,471
  • 149