Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit 845a1c8

Browse files
committed
fix: auth token lifecycle — expiry, consumption, and hash fixes (wd-2qc)
1. Verification tokens: add expires_at column, 24h expiry, delete after use (single-use), upgrade Hash() from MD5 to SHA-256 2. Remember-me logout: hash cookie value with SHA-256 before DB lookup (raw value was compared against stored hash — never matched) 3. RememberToken.createToken(): explicit SHA-256 instead of default MD5
1 parent a1883f6 commit 845a1c8

4 files changed

Lines changed: 64 additions & 14 deletions

File tree

app/controllers/web/AuthController.cfc

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,9 @@ component extends="app.Controllers.Controller" {
367367

368368
// Clear remember me token if exists
369369
if (structKeyExists(cookie, "remember_me")) {
370-
var token = cookie.remember_me;
371-
var rememberToken = model("RememberToken").findOne(where="token = '#token#'");
370+
var rawToken = cookie.remember_me;
371+
var hashedToken = hash(rawToken, "SHA-256");
372+
var rememberToken = model("RememberToken").findOne(where="token = '#hashedToken#'");
372373
if (isObject(rememberToken)) {
373374
rememberToken.delete();
374375
}
@@ -693,13 +694,14 @@ component extends="app.Controllers.Controller" {
693694
}
694695

695696
// Generate and save verification token
696-
var verificationToken = Hash(createUUID());
697-
697+
var verificationToken = Hash(createUUID(), "SHA-256");
698+
698699
var newToken = model("UserToken").new();
699700
newToken.token = verificationToken;
700701
newToken.user_id = userId; // Now a string, safe
701702
newToken.status = false;
702-
703+
newToken.expiresAt = dateAdd("h", 24, now());
704+
703705
if (!newToken.save()) {
704706
model("Log").log(
705707
category = "wheels.auth",
@@ -793,11 +795,12 @@ component extends="app.Controllers.Controller" {
793795

794796
if (!isObject(existingToken)) {
795797
// Generate a new verification token
796-
var verificationToken = Hash(createUUID());
798+
var verificationToken = Hash(createUUID(), "SHA-256");
797799
var newToken = model("UserToken").new();
798800
newToken.token = verificationToken;
799801
newToken.user_id = user.id;
800802
newToken.status = false; // Not verified
803+
newToken.expiresAt = dateAdd("h", 24, now());
801804
newToken.save();
802805
} else {
803806
var verificationToken = existingToken.token;
@@ -825,12 +828,21 @@ component extends="app.Controllers.Controller" {
825828

826829
private function verifyToken(required string token) {
827830
var message="";
828-
var token = model("UserToken").findOne(where="token = '#token#'");
831+
var tokenRecord = model("UserToken").findOne(where="token = '#token#'");
832+
833+
if (isObject(tokenRecord)) {
834+
// Check if token has expired
835+
if (isDate(tokenRecord.expiresAt) && dateCompare(now(), tokenRecord.expiresAt) > 0) {
836+
tokenRecord.delete();
837+
message = "false";
838+
return message;
839+
}
829840

830-
if (isObject(token)) {
831-
var user = model("User").findByKey(include="Role", key='#token.user_id#');
841+
var user = model("User").findByKey(include="Role", key='#tokenRecord.user_id#');
832842
if(isObject(user)){
833843
user.update(status=SetActive(), roleId=2); // Activate user, set editor role
844+
// Consume the token — single use only
845+
tokenRecord.delete();
834846
return user;
835847
}else{
836848
message="false";
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
component extends="wheels.migrator.Migration" hint="add expires_at column to user_tokens for token expiry" {
2+
3+
function up() {
4+
transaction {
5+
try {
6+
addColumn(table='user_tokens', columnType='datetime', columnName='expires_at', null=true);
7+
} catch (any e) {
8+
local.exception = e;
9+
}
10+
11+
if (StructKeyExists(local, "exception")) {
12+
transaction action="rollback";
13+
Throw(errorCode = "1", detail = local.exception.detail, message = local.exception.message, type = "any");
14+
} else {
15+
transaction action="commit";
16+
}
17+
}
18+
}
19+
20+
function down() {
21+
transaction {
22+
try {
23+
removeColumn(table='user_tokens', columnName='expires_at');
24+
} catch (any e) {
25+
local.exception = e;
26+
}
27+
28+
if (StructKeyExists(local, "exception")) {
29+
transaction action="rollback";
30+
Throw(errorCode = "1", detail = local.exception.detail, message = local.exception.message, type = "any");
31+
} else {
32+
transaction action="commit";
33+
}
34+
}
35+
}
36+
37+
}

app/models/RememberToken.cfc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ component extends="app.Models.Model" {
6767
public function createToken(required numeric userId) {
6868
var token = new();
6969
token.userId = arguments.userId;
70-
token.token = hash(createUUID() & arguments.userId & now());
70+
token.token = hash(createUUID() & arguments.userId & now(), "SHA-256");
7171
token.expiresAt = dateAdd("d", 30, now());
7272
return token.save();
7373
}
7474

75-
// Find token by value
76-
public function findByToken(required string token) {
75+
// Find token by hashed value (caller must hash raw cookie value with SHA-256 first)
76+
public function findByToken(required string hashedToken) {
7777
return findOne(
78-
where="token = '#arguments.token#' AND expiresAt > '#dateTimeFormat(now(), "yyyy-MM-dd HH:nn:ss")#'"
78+
where="token = '#arguments.hashedToken#' AND expiresAt > '#dateTimeFormat(now(), "yyyy-MM-dd HH:nn:ss")#'"
7979
);
8080
}
8181

app/models/UserToken.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ component extends="app.Models.Model" {
99
property(name="updatedAt", column="updatedat", dataType ="datetime", defaultValue = "");
1010
property(name="deletedAt", column="deletedat", dataType ="datetime", defaultValue = "");
1111
property(name="user_id", column="user_id", dataType ="string");
12-
12+
property(name="expiresAt", column="expires_at", dataType ="datetime", defaultValue = "");
13+
1314
belongsTo(name="User", foreignKey="user_id");
1415
}
1516

0 commit comments

Comments
 (0)