Skip to content

Strengthened private login with TOTP

Ticket #377: Strengthening login to the application's private interface.
Type: Feature and private-access protection.
Affected Component: Login, portfolio access, activation and access recovery.

1. Context and objectives

The application has a private interface containing my real investment portfolio data. Login relied on a username and password. I wanted to strengthen this protection without adding a complicated workflow or depending on an SMS or email delivery service.

The goal was to prevent a stolen password from being sufficient to access the portfolio. I chose TOTP two-factor authentication: a temporary code produced by an authenticator application on the phone, in addition to the password.

This choice remains proportionate to a personal application: a single form, administered activation and server-side recovery if the phone is lost.

2. Improvements implemented

I added the temporary code to the existing login form. I now enter my username, my password and my authentication code on the same screen. After activation, private access is granted only when both factors are valid.

Three safeguards complement this workflow:

  • Repeated attempts are slowed down. Five authentication failures within five minutes result in a five-minute wait. An additional limit applies to attempts from the same origin.
  • An accepted code cannot be reused during its validity period. Two simultaneous requests must not allow two logins using that same code.
  • Previous logins lose access when the factor is activated or replaced. I must then log in again with the current protection.

The portfolio pages, transactions and dashboard retain their behavior after login. The public demonstration page remains independent of this private area.

3. Reliable access and recovery

Because adding a second factor also changes how access is activated and recovered, I documented those two paths separately below.

Activation confirmed before taking effect

I provided activation through the server's administrative terminal. I register the key in my authenticator application, then confirm a first code. Protection is declared active only after this verification.

The secret used to generate codes is encrypted on the server. It appears neither on login pages, nor in browser cookies, nor in authentication logs. A verification error does not restore password-only login.

Recovery without weakening protection

If I lose the phone, I retain a server-side replacement procedure. No public recovery screen or backup code is added.

The previous factor remains active until the new one is confirmed. After replacement, the old factor and previous sessions no longer grant access to the portfolio. This procedure requires retaining administrative server access independent of the lost phone.

The detailed rules are presented in the authentication documentation and the activation and replacement guide.

4. Checks and their limitations

I checked the workflows using data separate from the real portfolio. The checks cover incorrect or expired codes, reuse, simultaneous requests, waiting periods, activation, replacement and loss of access for previous sessions.

Check Local result
Baseline before the changes 429 tests passed and 83.66% coverage, measured on an isolated copy of the baseline version.
Full automated test suite 525 tests passed, 7 skipped, no failures.
Code covered by tests 85.28%, compared with an initial 83.66%. This measures lines, not branches, and does not guarantee the absence of defects.
Authentication service 93.31% line coverage.
Workflows in Google Chrome 31 passed, covering TOTP login and transactions on an isolated local database.
Persistence and simultaneous requests on SQLite Code reuse rejection, single confirmation and state retention after restart verified.

TypeScript checking and quality checks on the modified browser files also passed. The global check still reports six errors in unmodified files. The quality score calculated using the project's formula is 92.31/100; the seven skipped tests are not counted as successes.

These skipped tests correspond to six unavailable MariaDB checks and one migration check not applicable to SQLite. Local results therefore do not replace verification of simultaneous requests on the database used in production.

The review also revealed that a semicolon in the migration comment was interpreted as an SQL separator. I replaced it with a full stop and added a regression checking the statements sent to the connection. I also aligned the estimated duration in French and English and updated the constitution to match the token usage summary chosen for my reports.

Filling in the Copilot credits then exposed an overly rigid documentation test: it still required empty cells. I updated it to check that FR/EN data matches, whether filled in or not, without imposing an old value. When a cost is included, it must also match between both versions.

5. Production incident, service restoration and activation

After the November 1, 2026 deployment, the private website became unavailable. The November 3 investigation confirmed 502 Bad Gateway responses from the website and login page, while documentation remained accessible. The application container repeatedly restarted before Gunicorn launched; its counter exceeded 1,600 restarts.

Cause: incomplete production configuration

SECRET_KEY was present, but TOTP_ENCRYPTION_KEY was missing from the production environment. The service attempted to convert this missing value with .encode("ascii"), raising an AttributeError. Validation neither explicitly checked this case nor handled this exception. The thirty bootstrap attempts therefore repeated a permanent error, after which the process exited without starting the web server. Nginx consequently received no usable response from its backend.

Read-only inspection ruled out a forgotten migration as the immediate cause: migration 0005 was recorded, the TOTP columns, rate-limiting table and its index existed, and MariaDB was accessible. An isolated reproduction confirmed the exact case: a signing key present and a TOTP key absent.

This configuration difference also explains why checks passed without preventing the outage. Local checks supplied generated keys; configuration tests covered an empty or malformed TOTP key, but not its absence while retaining the signing key. The deployment workflow was green: after launching Docker, it checked text in files without waiting for an HTTP response or checking process stability. Deployment success therefore did not prove website availability.

Implemented solution: restore service without resetting the portfolio

The initial investigation changed neither files nor business data. Once targeted service restoration was authorized, I first verified that no active or pending TOTP factor required recovery of an earlier key. I then provisioned a persistent encryption key, independent of the session key, without displaying it, with a backup separate from the database and root-only permissions.

To also support HTTPS login, I checked the Nginx configuration and backend restricted to 127.0.0.1:8000. With Nginx as the sole proxy, AUTH_PROXY_HOPS=1 now allows Flask to recognize the forwarded HTTPS protocol without trusting a hostname supplied through a proxy header. The HTTPS requirement and cookie protections remain enabled.

Finally, I recreated only the application container, without recreating MariaDB or its volumes. The prior dry run and startup log confirmed zero transactions added. The following checks provided evidence of recovery:

Check on November 3, 2026 Result
HTTPS login page 200, TLS certificate verified and TOTP form present.
Private access without a session Redirect to login; HTTP redirected to HTTPS.
Stability after readiness Two minutes without restarting, with seven successful HTTPS responses.
Login cookies and caching Secure, HttpOnly, SameSite=Lax and Cache-Control: no-store verified.
Database and volumes Same MariaDB container and volume; unchanged counts: 140 holdings, 27,358 historical records and 402 transactions.
Migrations Five recorded migrations; no new migration applied.

These measurements establish observed recovery, not a guarantee of long-term stability. No validation-code fix or deployment-workflow hardening was performed during this targeted intervention; both defects still require correction, with a missing-key regression and a real availability check.

Activation: distinguish the server key from the account factor

After recovery, login without a code succeeded. This behavior followed the account state: the TOTP factor was not yet activated. The server encryption key does not replace phone registration; while totp_enabled is false, the code permits password-only login.

I therefore clarified the activation and replacement guide in French and English: check status, run begin from an interactive SSH terminal, register the key manually on the phone, confirm a code within ten minutes, then wait for the next code to test login. Confirmation consumes that first code and revokes previous sessions.

The subsequent read-only check, on November 3 at approximately 02:15 UTC, found one account with an active factor and no pending preparation. Activation is therefore confirmed in the database. This finding is not new evidence of successful two-factor login or password-only rejection after activation: those workflows require separate verification without publishing any key or code.

6. Lessons Learned

This work helped me identify five habits for working more effectively with Copilot:

  • Specify the deliverables. From the start, I list the code, tests, FR/EN documents, wiki and report, with evidence of verification for each.
  • Name the remaining work. Instead of simply saying “continue”, I identify the remaining tasks and the stopping point, such as before deployment.
  • Check instructions when something is missed. I check the project's rules and any contradictions before repeating a request.
  • Share the failure link. For a test or deployment problem, I provide the GitHub link and expected result rather than copying a long log.
  • Distinguish delivery from operation. I check the actual configuration, HTTPS response and account activation rather than inferring their success from a green workflow.

These habits aim to reduce follow-up requests and unnecessary rereading; the summary below retains the available consumption data.

7. Token usage summary

Consumption tracking covers the whole of issue #377: preparation, implementation, checks and documentation.

The available figures below are retained; no additional consumption measurement is available for the November 3 incident and its documentation.

Tracked item Summary status
Working session duration (estimate) 3 hours
Copilot AI credits consumed 1041

References