Different life time of user tokens for invites and forgotten passwords.

main 1.2.0
Johan Carlberg 2026-02-20 21:45:30 +01:00
parent 228a710f1e
commit 878a28c89f
5 changed files with 18 additions and 11 deletions

View File

@ -20,6 +20,7 @@ CREATE TABLE "user_tokens" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL, "user_id" UUID NOT NULL,
"token" CHARACTER VARYING (1000) NOT NULL, "token" CHARACTER VARYING (1000) NOT NULL,
"realm" CHARACTER VARYING (10) NOT NULL CHECK ("realm" IN ('invite', 'forgot')),
"insert_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "insert_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_tokens_user_fk" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE CONSTRAINT "user_tokens_user_fk" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
); );

View File

@ -13,9 +13,11 @@ WITH "created" AS (
SELECT "id", 'admin' SELECT "id", 'admin'
FROM "created" FROM "created"
) )
INSERT INTO "user_tokens" ("user_id", "token") INSERT INTO "user_tokens" ("user_id", "token", "realm")
SELECT "id", (SELECT string_agg (substr (c, (random() * length (c) + 1)::integer, 1), '') AS "token" SELECT "id",
(SELECT string_agg (substr (c, (random() * length (c) + 1)::integer, 1), '') AS "token"
FROM (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) AS x(c), FROM (VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')) AS x(c),
generate_series (1, 32)) generate_series (1, 32)),
'invite'
FROM "created" FROM "created"
RETURNING 'https://example.com/auth/password/' || "token" AS "Password URL" RETURNING 'https://example.com/auth/password/' || "token" AS "Password URL"

View File

@ -49,8 +49,8 @@ extension ManagedUser {
TRUE) TRUE)
RETURNING "id" RETURNING "id"
) )
INSERT INTO "user_tokens" ("user_id", "token") INSERT INTO "user_tokens" ("user_id", "token", "realm")
SELECT "id", \(bind: token) SELECT "id", \(bind: token), 'invite'
FROM created FROM created
""") """)
.run() .run()
@ -72,8 +72,8 @@ extension ManagedUser {
FROM "created" FROM "created"
CROSS JOIN unnest (\(bind: roles)) AS "role_name" CROSS JOIN unnest (\(bind: roles)) AS "role_name"
) )
INSERT INTO "user_tokens" ("user_id", "token") INSERT INTO "user_tokens" ("user_id", "token", "realm")
SELECT "id", \(bind: token) SELECT "id", \(bind: token), 'invite'
FROM "created" FROM "created"
""") """)
.run() .run()
@ -121,8 +121,8 @@ extension ManagedUser {
public static func store (token: String, userId: UUID, on connection: any SQLDatabase) async throws { public static func store (token: String, userId: UUID, on connection: any SQLDatabase) async throws {
try await connection.raw(""" try await connection.raw("""
INSERT INTO "user_tokens" ("user_id", "token") INSERT INTO "user_tokens" ("user_id", "token", "realm")
VALUES (\(bind: userId), \(bind: token)) VALUES (\(bind: userId), \(bind: token), 'forgot')
""") """)
.run() .run()
} }

View File

@ -30,7 +30,9 @@ struct UserToken: Decodable {
JOIN "users" JOIN "users"
ON "users"."id" = "user_tokens"."user_id" ON "users"."id" = "user_tokens"."user_id"
WHERE "token" = \(bind: token) WHERE "token" = \(bind: token)
AND "insert_time" >= CURRENT_TIMESTAMP - INTERVAL '1 HOUR' AND "insert_time" >= CURRENT_TIMESTAMP - CASE WHEN "realm" = 'invite'
THEN INTERVAL '1 DAY'
ELSE INTERVAL '1 HOUR' END
""") """)
.first (decoding: Token.self) .first (decoding: Token.self)
} }

View File

@ -9,6 +9,8 @@ struct AuthorizationTests {
private func withApp(_ test: (Application) async throws -> ()) async throws { private func withApp(_ test: (Application) async throws -> ()) async throws {
let app = try await Application.make (.testing) let app = try await Application.make (.testing)
do { do {
setenv ("BASE_URL", "http://localhost", 0)
setenv ("EMAIL_SENDER", "nobody@example.com", 0)
try await SampleApp.configure (app) try await SampleApp.configure (app)
let mockDatabase = MockDatabase (eventLoop: app.eventLoopGroup.next()) let mockDatabase = MockDatabase (eventLoop: app.eventLoopGroup.next())
app.storage[Application.MockDatabaseKey.self] = mockDatabase app.storage[Application.MockDatabaseKey.self] = mockDatabase