Skip to content

Conversation

@AryanGodara
Copy link
Member

@AryanGodara AryanGodara commented May 12, 2025

Linked Issues

  • closes NOISSUE

Description

This PR implements authentication functionality for the leaderboard backend, enabling users to authenticate using wallet signatures. The implementation includes:

  • Added an auth_sessions table to store user sessions
  • Created an AuthRepository with methods for challenge generation, signature verification, and session management
  • Implemented authentication routes for challenge requests, signature verification, session management, and user info retrieval
  • Added middleware to validate authentication tokens
  • Updated environment variables to support authentication configuration

The authentication flow follows a challenge-response pattern where users request a challenge message, sign it with their wallet, and submit the signature to verify their identity and create a session.

Toggle Checklist

Checklist

Basics

  • B1. I have applied the proper label & proper branch name (e.g. norswap/build-system-caching).
  • B2. This PR is not so big that it should be split & addresses only one concern.
  • B3. The PR targets the lowest branch it can (ideally master).

Reminder: PR review guidelines

Correctness

  • C1. Builds and passes tests.
  • C2. The code is properly parameterized & compatible with different environments (e.g. local,
    testnet, mainnet, standalone wallet, ...).
  • C3. I have manually tested my changes & connected features.

Tested in Chrome with local development environment. Verified authentication flow with wallet signature and session management.

Tested scenarios:

  • Challenge request and signature verification

  • Session creation and validation

  • User info retrieval

  • Session listing and logout

  • C4. I have performed a thorough self-review of my code after submitting the PR,
    and have updated the code & comments accordingly.

Architecture & Documentation

  • D1. I made it easy to reason locally about the code, by (1) using proper abstraction boundaries,
    (2) commenting these boundaries correctly, (3) adding inline comments for context when needed.
  • D2. All public-facing APIs are documented in the docs.
    Public APIS and meaningful (non-local) internal APIs are properly documented in code comments.
  • D3. If appropriate, the general architecture of the code is documented in a code comment or
    in a Markdown document.
  • D4. An appropriate Changeset has been generated (and committed) with make changeset for
    breaking and meaningful changes in packages (not required for cleanups & refactors).

Copy link
Member Author

AryanGodara commented May 12, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@AryanGodara AryanGodara changed the title intial commit, setup code for auth, in progress [skip ci] Add authentication system with wallet-based sessions May 12, 2025
@AryanGodara AryanGodara changed the title Add authentication system with wallet-based sessions Add authentication to Leaderboard backend with wallet-based sessions May 12, 2025
@AryanGodara AryanGodara self-assigned this May 12, 2025
@AryanGodara AryanGodara changed the base branch from aryan/swarm-leadership-page-mvp to graphite-base/753 May 12, 2025 13:58
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 262b3bc to 9b24778 Compare May 13, 2025 09:24
@AryanGodara AryanGodara changed the base branch from graphite-base/753 to aryan/swarm-leadership-page-mvp May 13, 2025 09:24
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 9b24778 to 6164f58 Compare May 13, 2025 09:38
@norswap norswap added the draft Not ready for review label May 16, 2025
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from b3b1875 to 04fe2d6 Compare May 19, 2025 12:20
@AryanGodara AryanGodara force-pushed the aryan/swarm-leadership-page-mvp branch from a2c3cc7 to 66315ae Compare May 19, 2025 12:20
@AryanGodara AryanGodara changed the base branch from aryan/swarm-leadership-page-mvp to graphite-base/753 May 20, 2025 06:20
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 04fe2d6 to d7b5c98 Compare May 20, 2025 06:20
@AryanGodara AryanGodara changed the base branch from graphite-base/753 to aryan/swarm-leadership-page-mvp May 20, 2025 06:53
@AryanGodara AryanGodara force-pushed the aryan/swarm-leadership-page-mvp branch from decea15 to b5445e1 Compare May 21, 2025 09:32
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 679daa4 to 9c6956d Compare May 21, 2025 09:32
@AryanGodara AryanGodara force-pushed the aryan/swarm-leadership-page-mvp branch from b5445e1 to 5e98b69 Compare May 21, 2025 10:17
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 9c6956d to a9fce85 Compare May 21, 2025 10:17
@AryanGodara AryanGodara added reviewing-1 Ready for, or undergoing first-line review Not Urgent and removed draft Not ready for review labels May 21, 2025
try {
const { primary_wallet } = c.req.valid("json")
// Use a hardcoded, Ethereum-style message for leaderboard authentication
const message = `\x19Leaderboard Signed Message:\nHappyChain Leaderboard Authentication Request for ${primary_wallet}`
Copy link
Contributor

Choose a reason for hiding this comment

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

this should include a unique nonce to protect against replay attacks i believe. don't need to follow the SIWE spec exactly i don't think, but the rational is good here, and its a good guide to follow https://eips.ethereum.org/EIPS/eip-4361#message-fields & https://docs.login.xyz/

Copy link
Member Author

Choose a reason for hiding this comment

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

Finally implemented this. After hrs of going through and implemented the ERC, found out that viem already had helper functions for this stuff 🫠

Copy link
Contributor

Choose a reason for hiding this comment

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

but i bet you understand it better now 😁

setCookie(c, "session_id", session.id, {
httpOnly: true,
secure: true,
domain: "localhost",
Copy link
Contributor

Choose a reason for hiding this comment

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

likely this should be a env variable :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yessir, refactored the env.ts and cookieCOnfig 🫡

Comment on lines +105 to +159
if (!user) {
return c.json({ ok: false, error: "User not found" }, 404)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

if this happened, then i think something catastrophic happened right? maybe want to clear the cookie and log them out or handle this in some way

... does this want to be part of the requireAuth middleware even?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this should ever happen. Maybe I should add more logging around it to know if it does.

But since user: User | undefined after fetching it from the repository, I can't not add this line of if (!user); residual habit from writing in golang 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added requireAuth to it, just so authenticated users can fetch themselves. I don't have any usecase for this endpoint right now. It's just present as a stub for now

Copy link
Contributor

Choose a reason for hiding this comment

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

i suspected that was the case :) yeah if something supposedly impossible happens like this, but you have a check anyways to fix types or whatever, should probably log when things go sideways, just in case 👍

try {
const session = await this.db
.selectFrom("auth_sessions")
.where("id", "=", sessionId)
Copy link
Contributor

Choose a reason for hiding this comment

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

you could add a whereExists('users.id', '=', 'auth_sessions.user_id') (i probably have the wrong syntax here) so that deleted users sessions are automatically invalidated

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated this, added an innerJoin, so that it makes sure the user still exists when fetching the session related to it

Comment on lines 46 to 50
await this.db
.updateTable("auth_sessions")
.set({ last_used_at: new Date().toISOString() })
.where("id", "=", sessionId)
.executeTakeFirstOrThrow()
Copy link
Contributor

Choose a reason for hiding this comment

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

you are taking first already, why not update and fetch in one go & save a query?

Suggested change
await this.db
.updateTable("auth_sessions")
.set({ last_used_at: new Date().toISOString() })
.where("id", "=", sessionId)
.executeTakeFirstOrThrow()
const session = await this.db
.updateTable("auth_sessions")
.set({ last_used_at: new Date().toISOString() })
.where("id", "=", sessionId)
.returningAll()
.executeTakeFirstOrThrow()
  • this works well in postres, unsure about sqlite

Copy link
Member Author

Choose a reason for hiding this comment

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

For this and the above comment, here's my new implementation :-

    async verifySession(sessionId: AuthSessionTableId): Promise<AuthSession | undefined> {
        try {
            // First check if the session exists and belongs to an existing user
            const session = await this.db
                .selectFrom("auth_sessions")
                .innerJoin("users", "users.id", "auth_sessions.user_id")
                .where("auth_sessions.id", "=", sessionId)
                .select(["auth_sessions.id", "auth_sessions.user_id", "auth_sessions.primary_wallet", 
                         "auth_sessions.created_at", "auth_sessions.last_used_at"])
                .executeTakeFirst()

            if (!session) {
                return undefined
            }

            // Update the last_used_at timestamp
            const currentTime = new Date().toISOString()
            
            // We don't need the result of this update since we already have the session data
            // and the only thing that changed is the last_used_at timestamp, which isn't usually
            // needed by the application logic
            await this.db
                .updateTable("auth_sessions")
                .set({ last_used_at: currentTime })
                .where("id", "=", sessionId)
                .executeTakeFirstOrThrow()

            // Return the session with the updated timestamp
            // The session object already has the correct types from the database schema
            // so we need to preserve those types
            return {
                ...session,
                last_used_at: new Date(currentTime) // Convert string to Date to match expected type
            }
        } catch (error) {
            console.error("Error verifying session:", error)
            return undefined
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

my comment was more that i think you can do it all in a single more efficient query :) I probably have syntax slightly wrong, and it simply might not work with sqlite, but something like this should have the same result I think?

    async verifySession(sessionId: AuthSessionTableId): Promise<AuthSession | undefined> {
        try {
           const session = await this.db
                .updateTable("auth_sessions")
                .set({ last_used_at: new Date().toISOString() })
                .where("id", "=", sessionId)
                .where(({ exists, selectFrom }) => 
                    exists(
                        selectFrom('users')
                            .select('users.id')
                            .whereRef('users.id', '=', 'auth_sessions.user_id')
                    )
                )
                .returning([
                    "auth_sessions.id", 
                    "auth_sessions.user_id", 
                    "auth_sessions.primary_wallet", 
                    "auth_sessions.created_at",
                    "auth_sessions.last_used_at"
                ])
                .executeTakeFirstOrThrow()

            return session
        } catch (error) {
            console.error("Error verifying session:", error)
            return undefined
        }
    }


async deleteSession(sessionId: AuthSessionTableId): Promise<boolean> {
try {
await this.db.deleteFrom("auth_sessions").where("id", "=", sessionId).executeTakeFirstOrThrow()
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of throwing, i think execute can return the number of rows deleted. and you can return true/false from that

if (walletEntry) {
user = await this.findById(walletEntry.user_id)
if (includeWallets) {
const wallets = await this.getUserWallets(user.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

would be nice if this was a join and a single query

@AryanGodara AryanGodara marked this pull request as ready for review May 23, 2025 11:10
@AryanGodara AryanGodara force-pushed the aryan/leaderboard-auth branch from 96e78dc to cb3c190 Compare May 23, 2025 11:15
@AryanGodara AryanGodara force-pushed the aryan/swarm-leadership-page-mvp branch from 5e98b69 to 74cfd9a Compare May 23, 2025 11:16
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented May 23, 2025

Deploying happychain with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4f63489
Status: ✅  Deploy successful!
Preview URL: https://e0a44d65.happychain.pages.dev
Branch Preview URL: https://aryan-leaderboard-auth.happychain.pages.dev

View logs

example: {
primary_wallet: "0xBC5F85819B9b970c956f80c1Ab5EfbE73c818eaa",
message:
"happychain.app wants you to sign in with your Ethereum account:\n0xBC5F85819B9b970c956f80c1Ab5EfbE73c818eaa\n\nSign this message to authenticate with HappyChain Leaderboard. This will not trigger a blockchain transaction or cost any gas fees.\n\nURI: https://happychain.app/login\nVersion: 1\nChain ID: 216\nNonce: 7f8e9d1c6b3a2e5f4d7c8b9a1e3f5d7c\nIssued At: 2025-05-23T09:30:00.000Z\nExpiration Time: 2025-05-23T09:35:00.000Z",
Copy link
Contributor

Choose a reason for hiding this comment

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

not a bit deal, but this might be nicer as a template string. (maybe this doesn't play as nice with the docs though? not sure)

Suggested change
"happychain.app wants you to sign in with your Ethereum account:\n0xBC5F85819B9b970c956f80c1Ab5EfbE73c818eaa\n\nSign this message to authenticate with HappyChain Leaderboard. This will not trigger a blockchain transaction or cost any gas fees.\n\nURI: https://happychain.app/login\nVersion: 1\nChain ID: 216\nNonce: 7f8e9d1c6b3a2e5f4d7c8b9a1e3f5d7c\nIssued At: 2025-05-23T09:30:00.000Z\nExpiration Time: 2025-05-23T09:35:00.000Z",
message: `
happychain.app wants you to sign in with your Ethereum account:
0xBC5F85819B9b970c956f80c1Ab5EfbE73c818eaa
Sign this message to authenticate with HappyChain Leaderboard. This will not trigger a blockchain transaction or cost any gas fees.
URI: https://happychain.app/login
Version: 1
Chain ID: 216
Nonce: 7f8e9d1c6b3a2e5f4d7c8b9a1e3f5d7c
Issued At: 2025-05-23T09:30:00.000Z
Expiration Time: 2025-05-23T09:35:00.000Z",

also out of curiosity is happychain.app correct?

Copy link
Member Author

@AryanGodara AryanGodara May 23, 2025

Choose a reason for hiding this comment

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

I might be wrong, but there was some rule that we can't use \n or 0xa0 or whatever character it is, inside this message.

And yeah, the happychain.app and other values are all stub😅, they're mostly optional too

Copy link
Contributor

Choose a reason for hiding this comment

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

you are using \n not me 😅 but doesn't matter haha, was just a thought as i was reading through. its fine 🤝

Copy link
Member Author

Choose a reason for hiding this comment

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

Will change this😭🙇🏻‍♂️🙇🏻‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

haha idk, mine might not work

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

Labels

reviewing-1 Ready for, or undergoing first-line review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants