Skip to content

fix: feat(auth): token flow end-to-end, DB allowlist, rolling expiration (#58)#190

Open
IliyaBrook wants to merge 6 commits intoRichardAtCT:mainfrom
IliyaBrook:fix/58-token-auth-end-to-end
Open

fix: feat(auth): token flow end-to-end, DB allowlist, rolling expiration (#58)#190
IliyaBrook wants to merge 6 commits intoRichardAtCT:mainfrom
IliyaBrook:fix/58-token-auth-end-to-end

Conversation

@IliyaBrook
Copy link
Copy Markdown

Summary

Closes #58. The token auth infrastructure was fully implemented in main but never wired end-to-end — three gaps blocked real use. This PR closes those gaps and adds a second admin flow (DB-backed allowlist) that fits Telegram UX better than the token dance alone. It also fixes a UX issue with token expiration semantics.

What was added

1. Token flow (closes the three gaps from #58)

  • /auth <token> — users can finally present a token
  • Auth middleware extracts the token from /auth <token> and passes it as credentials={"token": ...} to authenticate_user() (was always {} before)
  • InMemoryTokenStorage replaced with SqliteTokenStorage backed by the existing user_tokens table — tokens now survive restarts

2. New admin commands for token flow

Command Purpose
/auth generate <user_id> Create a token for a user
/auth revoke <user_id> Revoke a user's token

Admin responses:

/auth generate <user_id>:

🔑 Token generated for user <user_id>:
<token>
The user should send:
/auth <token>
Expires in 30 days.

/auth revoke <user_id> (user has active token):

✅ Token revoked for user <user_id>.

/auth revoke <user_id> (no active token in DB):

ℹ️ User <user_id> has no active token — nothing to revoke.

Previously this always said "Token revoked" even when nothing was revoked.

3. New DB-backed allowlist (no token dance required)

The token dance (generate → copy → paste) is the classic pattern for cross-channel auth (e.g., CLI admin + web user), but it's overkill inside Telegram where user_id is already platform-verified. Added a simpler admin-managed allowlist using the already-existing users.is_allowed column — a user added this way can just write to the bot, no token required.

Command Purpose
/auth add <user_id> Add user to persistent allowlist
/auth remove <user_id> Remove user from allowlist

Admin responses:

/auth add <user_id>:

✅ User <user_id> added to the allowlist. They can now write to the bot.

/auth remove <user_id>:

✅ User <user_id> removed from the allowlist.

/auth remove for a user not in the allowlist:

ℹ️ User <user_id> is not in the allowlist — nothing to remove.

Backed by a new DatabaseAllowlistAuthProvider that sits between the env-based WhitelistAuthProvider (permanent admins) and TokenAuthProvider in the auth chain.

4. Utility commands

  • /auth or /auth status — show the caller's own auth status
  • /auth <garbage> from an admin now shows a help hint with the real subcommands instead of a misleading "Authenticated successfully"

Rolling token expiration (UX fix)

The original behavior was overkill for Telegram

The inherited design used a fixed 30-day token expiration combined with a 24-hour in-memory session. That's a reasonable "defense in depth" for web/mobile apps, but it produced a real pain point inside Telegram:

  1. User runs /auth <token> — authenticated, session lasts 24h.
  2. Session refreshes on every message within the 24h window — fine.
  3. User goes quiet for 24+ hours. Session expires.
  4. User writes again → middleware re-authenticates, no credentials are in the message → falls through every provider → "Authentication Required".
  5. User is forced to paste /auth <token> again, roughly once per day of inactivity.

In Telegram this extra check buys nothing: every message already carries a user_id that the Telegram platform verifies. The token is a one-time proof that the admin approved this user_id — re-verifying it every 24h doesn't add any real security, it only annoys legitimate users.

What changed

  1. TokenAuthProvider.authenticate() now accepts empty credentials when the caller's user_id has a non-expired active token in the DB. The first /auth <token> is still required to establish the link; after that, the presence of an active stored token is enough. A wrong raw token is still rejected — the fallback only kicks in when no raw token is supplied.
  2. Expiration slides forward on every successful auth. Each time authenticate() succeeds, expires_at is pushed to now + 30 days and last_used is updated (the last_used column was in the schema from day one but nothing ever wrote to it).

Practical behavior

  • Active user, any cadence (hourly / daily / every 3 weeks): writes once with /auth <token>, then just keeps using the bot. The token silently refreshes whenever the 24h session lapses, so no more repeated paste-the-token dance.
  • User dormant for 30+ days: token expires, their next message gets "Authentication Required", and they need a fresh token from admin (/auth generate).
  • Admin wants to cut off access immediately: /auth revoke <user_id> still works exactly as before — session ends, next message is rejected.

Two ways to grant access (pick what fits)

Simpler — persistent allowlist (recommended for trusted users):

Admin: /auth add 12345
User:  (just writes — authenticated immediately)

Token-based (for time-boxed / out-of-band distribution):

Admin: /auth generate 12345   → gets a token, shares it out-of-band
User:  /auth <token>          → authenticated, rolling 30-day window

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

token auth infrastructure is complete but not usable end-to-end

1 participant