Skip to content

Single User Auth #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 9, 2025
Merged

Single User Auth #202

merged 16 commits into from
Aug 9, 2025

Conversation

stordahl
Copy link
Collaborator

@stordahl stordahl commented Jul 23, 2025

Overview

This PR introduces single user authentication for the dashboard. The intention is to allow a single password to be set during install, and then use that password to authenticate a user. The flow works as follows...

  • During npx @counterscale/cli install, the CLI will prompt the user to optionally enable authentication to access the dashboard. If they choose "yes", they will be prompted to input their desired password
    • This sets a Cloudflare Secret under the key CF_AUTH_ENABLED
  • The CLI then generates a unique secret that is used for verifying JWTs from cookies. This secret is stored in Cloudflare Secrets under the key CF_JWT_SECRET
  • The CLI then generates a salt, hashes the password, and stores the hashed password in Cloudflare Secrets under the key CF_PASSWORD_HASH
  • Once the secrets are set, and the new server version is deployed, the server uses the secrets to authenticate user input of a password, and managing JWTs in cookies

Major Changes

@counterscale/cli

  • Adds auth module that handles generating the hashed password and CF_JWT_SECRET
  • Integrates auth module with the install command
  • Adds a new npm script for generating CF_JWT_SECRET and CF_PASSWORD_HASH for use in .dev.vars for local development
    cd packages/cli && pnpm run generate-secrets

@counterscale/server

  • Adds auth and session modules that handle user login/logout including JWT creation/signing and verification via RR loaders/ actions
  • Adds a login form to the index route, conditionally rendered only if no user is authenticated
  • Adds a logout button to the header, conditionally rendered only if a user is authenticated

Copy link

codecov bot commented Jul 23, 2025

Codecov Report

❌ Patch coverage is 62.46246% with 125 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.64%. Comparing base (61ebd84) to head (7c38ca8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/cli/src/install.ts 25.24% 77 Missing ⚠️
packages/cli/scripts/generate-secrets.mjs 0.00% 38 Missing and 1 partial ⚠️
packages/server/app/routes/_index.tsx 92.85% 4 Missing ⚠️
packages/server/app/root.tsx 57.14% 3 Missing ⚠️
packages/server/app/lib/auth.ts 97.87% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #202      +/-   ##
==========================================
- Coverage   80.09%   78.64%   -1.45%     
==========================================
  Files          44       49       +5     
  Lines        3381     3695     +314     
  Branches      425      483      +58     
==========================================
+ Hits         2708     2906     +198     
- Misses        665      780     +115     
- Partials        8        9       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@stordahl
Copy link
Collaborator Author

@benvinegar Will work on increasing test coverage, but would love your feedback on the implementation!

@benvinegar benvinegar requested a review from Copilot July 23, 2025 06:39
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements single user authentication via a secret password for the Counterscale application. It adds login/logout functionality with session management and protects dashboard routes from unauthorized access.

  • Adds authentication system with password-based login and session storage
  • Protects all dashboard and resource routes with authentication middleware
  • Updates UI to show login form and logout link based on authentication state

Reviewed Changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/server/app/lib/auth.ts Core authentication functions for login, logout, and auth checks
packages/server/app/lib/session.ts Session storage configuration using cookies
packages/server/app/routes/_index.tsx Login form UI and authentication logic
packages/server/app/routes/logout.tsx Logout route handler
packages/server/app/routes/dashboard.tsx Added authentication requirement
packages/server/app/routes/resources.*.tsx Added authentication requirement to all resource routes
packages/server/app/root.tsx Added user state and logout link
packages/cli/src/install.ts Added app password prompt during installation
vitest.config.ts Added process isolation for tests
Various test files Updated tests to mock authentication and handle new requirements

Copy link
Owner

@benvinegar benvinegar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, there's two major things that need to change.

No plaintext passwords in CF secrets

We can't store plaintext passwords, even if it's stored in CF secret – it has to be salted + hashed so it is non-retrievable.

That means that when the user submits a password to log in, we must similarly salt + hash that and compare it to the stored/hashed password.

The "salt" is a deployment-specific value that needs to be generated during CLI installation/deployment. It could be randomly generated for simplicity. (This can be the same secret value we use for JWTs, re: section below.)

If the salt is randomly generated behind the scenes (user doesn't know it), and the CF secret got deleted or changed somehow, that would mean we'd lose our ability to validate user-submitted passwords going forward. But I think that's okay, because redeploying should fix it (we generate a new random salt).

No plaintext values in session cookie

We can't have the cookie be trivially inspectable or modifiable. Otherwise users could 1) examine the value and see a plaintext password, or 2) modify the length of their expiry by toggling values.

The solution for this is to use JSON Web Tokens (JWTs), which stores 1) that the user has an active, valid session and 2) the length of the session. These tokens are encrypted so they don't expose the data inside or let users modify that data.

I made a lot of progress JWTs in my branch. Take a look at createToken and verifyToken in app/lib/auth.ts.

@stordahl stordahl changed the title Single User Auth via Secret Single User Auth Jul 25, 2025
Copy link
Owner

@benvinegar benvinegar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments.

@stordahl
Copy link
Collaborator Author

Noting that I'll make a follow up PR adding a CLI command to roll/update the password

@stordahl stordahl marked this pull request as ready for review July 27, 2025 01:00
@stordahl stordahl requested a review from benvinegar July 27, 2025 02:53
@stordahl stordahl requested a review from benvinegar July 30, 2025 15:19
@stordahl stordahl mentioned this pull request Jul 30, 2025
@stordahl
Copy link
Collaborator Author

stordahl commented Aug 2, 2025

@benvinegar I think this is ready to merge. I just added a small refactor to the cli to explicitly check if certain secrets exist which will allow existing deploys to be updated with a password by running the install command. I ran the CLI locally to update my production deployment and auth is working deployed after setting password with the CLI.

FYI, I do want to fast follow with a new cli command for updating password/revoking existing JWTs, but don't want to bloat this PR more

@benvinegar
Copy link
Owner

@stordahl Hey I've just been slammed, didn't get a chance to run it. But I see my asks have been addressed.

Copy link
Owner

@benvinegar benvinegar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, last thing:

Enter the password you will use to access the Counterscale Dashboard

We have to make setting a password optional. (You could do "leave blank for none" but I think it's better to have a Y/N step.)

This is pretty important. For example, I think my demo site will basically stop working if it needs a password to access.

@benvinegar
Copy link
Owner

So, I tested the choice to set a password or not. Looks great.

First time – no password, great.

Second time – prompt doesn't come back, no way to set a password ever again. I think I'd prefer we just prompt them everytime, because deploying is pretty infrequent.

@stordahl
Copy link
Collaborator Author

stordahl commented Aug 8, 2025

@benvinegar makes sense and is a trivial change!

@stordahl
Copy link
Collaborator Author

stordahl commented Aug 8, 2025

@benvinegar I think you observed that because you still had CF_JWT_SECRET and CF_PASSWORD_HASH in your secrets! I deleted my deployment, deployed with no password, then deployed again and I was able to set a password

@stordahl stordahl merged commit 412a4e0 into main Aug 9, 2025
3 checks passed
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.

2 participants