Brijesh's Git Server — whodis @ 9855dd11af38dc3c6f883757b5d07e963c3dc2e2

My own webauthn as a service, free of cost, Unlimited MAU

feat: passkeys for authentication for the service's website
Brijesh Wawdhane brijesh@wawdhane.com
Fri, 15 Nov 2024 06:22:14 +0530
commit

9855dd11af38dc3c6f883757b5d07e963c3dc2e2

parent

fd45c6d452a86cba84f205a0ac4d3215ee91cfa1

M core/cmd/api/main.gocore/cmd/api/main.go

@@ -15,7 +15,7 @@ )

func init() { db := database.New() - defer db.Close() + // defer db.Close() err := db.CreateTables(context.Background()) if err != nil {
M core/go.modcore/go.mod

@@ -4,6 +4,20 @@ go 1.23.0

require ( github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/cors v1.2.1 + github.com/go-webauthn/webauthn v0.11.2 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.24 ) + +require ( + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-webauthn/x v0.1.14 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/go-tpm v0.9.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect +)
M core/go.sumcore/go.sum

@@ -1,6 +1,36 @@

+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= +github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= +github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= +github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
M core/internal/database/create-tables.gocore/internal/database/create-tables.go

@@ -25,6 +25,11 @@ user_id TEXT NOT NULL,

public_key BLOB NOT NULL, credential_id BLOB NOT NULL, sign_count INTEGER NOT NULL, + aaguid BLOB, + clone_warning BOOLEAN NOT NULL DEFAULT false, + attachment TEXT, + backup_eligible BOOLEAN NOT NULL DEFAULT false, + backup_state BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) );`)
M core/internal/database/database.gocore/internal/database/database.go

@@ -2,10 +2,12 @@ package database

import ( "context" + "core/internal/models" "database/sql" "log" "os" + "github.com/go-webauthn/webauthn/webauthn" _ "github.com/joho/godotenv/autoload" _ "github.com/mattn/go-sqlite3" )

@@ -13,6 +15,16 @@

type Service interface { Close() error CreateTables(ctx context.Context) error + + // User-related methods + GetUserByID(ctx context.Context, id string) (*models.User, error) + GetUserByName(ctx context.Context, name string) (*models.User, error) + SaveUser(ctx context.Context, user *models.User) error + + // Credential-related methods + SaveCredential(ctx context.Context, credential *models.Credential) error + GetCredentialsForUser(ctx context.Context, userID string) ([]webauthn.Credential, error) + UpdateCredentialSignCount(ctx context.Context, credentialID []byte, signCount uint32) error } type service struct {
A core/internal/database/user.go

@@ -0,0 +1,181 @@

+package database + +import ( + "context" + "core/internal/models" + "database/sql" + "log" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/google/uuid" +) + +// GetUserByID retrieves a user by their ID +func (s *service) GetUserByID(ctx context.Context, id string) (*models.User, error) { + var user models.User + err := s.db.QueryRowContext(ctx, ` + SELECT id, name, display_name FROM users WHERE id = ? + `, id).Scan(&user.ID, &user.Name, &user.DisplayName) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // User not found + } + return nil, err + } + + // Load user's credentials + credentials, err := s.GetCredentialsForUser(ctx, user.ID) + if err != nil { + return nil, err + } + user.Credentials = credentials + + return &user, nil +} + +// GetUserByName retrieves a user by their username +func (s *service) GetUserByName(ctx context.Context, name string) (*models.User, error) { + var user models.User + err := s.db.QueryRowContext(ctx, ` + SELECT id, name, display_name FROM users WHERE name = ? + `, name).Scan(&user.ID, &user.Name, &user.DisplayName) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // User not found + } + return nil, err + } + + // Load user's credentials + credentials, err := s.GetCredentialsForUser(ctx, user.ID) + if err != nil { + return nil, err + } + user.Credentials = credentials + + return &user, nil +} + +// SaveUser saves a new user to the database +func (s *service) SaveUser(ctx context.Context, user *models.User) error { + _, err := s.db.ExecContext(ctx, ` + INSERT INTO users (id, name, display_name) VALUES (?, ?, ?) + `, user.ID, user.Name, user.DisplayName) + return err +} + +// SaveCredential saves a new credential to the database +func (s *service) SaveCredential(ctx context.Context, credential *models.Credential) error { + // Check if the user has any backup-eligible credentials + existingCreds, err := s.GetCredentialsForUser(ctx, credential.UserID) + if err != nil { + return err + } + + if len(existingCreds) == 0 { + credential.BackupEligible = true + } else { + credential.BackupEligible = false + } + + recordID := uuid.New().String() + + // Log the credential being saved + log.Printf("Saving credential: %+v", credential) + + _, err = s.db.ExecContext(ctx, ` + INSERT INTO credentials ( + id, + user_id, + public_key, + credential_id, + sign_count, + aaguid, + clone_warning, + attachment, + backup_eligible, + backup_state + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + recordID, + credential.UserID, + credential.PublicKey, + credential.CredentialID, + credential.SignCount, + credential.AAGUID, + credential.CloneWarning, + string(credential.Attachment), + credential.BackupEligible, + credential.BackupState, + ) + + if err != nil { + log.Printf("Error saving credential: %v", err) + return err + } + + log.Printf("Successfully saved credential with ID: %s", recordID) + return nil +} + +// GetCredentialsForUser retrieves all credentials for a given user +func (s *service) GetCredentialsForUser(ctx context.Context, userID string) ([]webauthn.Credential, error) { + rows, err := s.db.QueryContext(ctx, ` + SELECT + credential_id, + public_key, + sign_count, + aaguid, + clone_warning, + attachment, + backup_eligible, + backup_state + FROM credentials + WHERE user_id = ? + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var credentials []webauthn.Credential + for rows.Next() { + var cred models.Credential + var attachmentStr string + err := rows.Scan( + &cred.CredentialID, + &cred.PublicKey, + &cred.SignCount, + &cred.AAGUID, + &cred.CloneWarning, + &attachmentStr, + &cred.BackupEligible, + &cred.BackupState, + ) + if err != nil { + return nil, err + } + cred.UserID = userID + cred.Attachment = protocol.AuthenticatorAttachment(attachmentStr) + + // Log the credential details for debugging + log.Printf("Retrieved credential from DB: %+v", cred) + + wanCred := cred.ToWebauthnCredential() + log.Printf("Converted to WebAuthn credential: %+v", wanCred) + + credentials = append(credentials, wanCred) + } + return credentials, rows.Err() +} + +// UpdateCredentialSignCount updates the signCount for a given credential +func (s *service) UpdateCredentialSignCount(ctx context.Context, credentialID []byte, signCount uint32) error { + _, err := s.db.ExecContext(ctx, ` + UPDATE credentials + SET sign_count = ? + WHERE credential_id = ? + `, signCount, credentialID) + return err +}
A core/internal/models/credential.go

@@ -0,0 +1,55 @@

+package models + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" +) + +// Credential represents a WebAuthn credential +type Credential struct { + ID string // database record ID + UserID string // foreign key to users table + PublicKey []byte // stored public key + CredentialID []byte // WebAuthn credential ID + SignCount uint32 + AAGUID []byte + CloneWarning bool + Attachment protocol.AuthenticatorAttachment + BackupEligible bool + BackupState bool +} + +type CredentialFlags struct { + // Flag UP indicates the users presence. + UserPresent bool `json:"userPresent"` + + // Flag UV indicates the user performed verification. + UserVerified bool `json:"userVerified"` + + // Flag BE indicates the credential is able to be backed up and/or sync'd between devices. This should NEVER change. + BackupEligible bool `json:"backupEligible"` + + // Flag BS indicates the credential has been backed up and/or sync'd. This value can change but it's recommended + // that RP's keep track of this value. + BackupState bool `json:"backupState"` +} + +// ToWebauthnCredential converts our Credential to a webauthn.Credential +func (c *Credential) ToWebauthnCredential() webauthn.Credential { + return webauthn.Credential{ + ID: c.CredentialID, + PublicKey: c.PublicKey, + Flags: webauthn.CredentialFlags{ + UserPresent: true, + UserVerified: true, + BackupEligible: c.BackupEligible, + BackupState: c.BackupState, + }, + Authenticator: webauthn.Authenticator{ + SignCount: c.SignCount, + AAGUID: c.AAGUID, + CloneWarning: c.CloneWarning, + Attachment: c.Attachment, + }, + } +}
A core/internal/models/user.go

@@ -0,0 +1,41 @@

+package models + +import ( + "github.com/go-webauthn/webauthn/webauthn" +) + +// User represents a user in our system. +type User struct { + ID string // Unique identifier for the user + Name string // Username + DisplayName string // Full name or display name + Credentials []webauthn.Credential // WebAuthn credentials +} + +// Ensure User satisfies the webauthn.User interface +var _ webauthn.User = &User{} + +// WebAuthnID returns the user's unique ID +func (u *User) WebAuthnID() []byte { + return []byte(u.ID) +} + +// WebAuthnName returns the user's username +func (u *User) WebAuthnName() string { + return u.Name +} + +// WebAuthnDisplayName returns the user's display name +func (u *User) WebAuthnDisplayName() string { + return u.DisplayName +} + +// WebAuthnIcon returns the user's icon URL (optional) +func (u *User) WebAuthnIcon() string { + return "" +} + +// WebAuthnCredentials returns the user's credentials +func (u *User) WebAuthnCredentials() []webauthn.Credential { + return u.Credentials +}
A core/internal/server/handlers.go

@@ -0,0 +1,272 @@

+package server + +import ( + "core/internal/models" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/google/uuid" +) + +// BeginRegistration starts the registration process +func (s *Server) BeginRegistration(w http.ResponseWriter, r *http.Request) { + var req struct { + Username string `json:"username"` + DisplayName string `json:"displayName"` + } + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil || req.Username == "" || req.DisplayName == "" { + log.Printf("Invalid request payload: %v", err) + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + // Create a new user + userID := uuid.New().String() + user := &models.User{ + ID: userID, + Name: req.Username, + DisplayName: req.DisplayName, + } + + // Save the user to the database + err = s.db.SaveUser(r.Context(), user) + if err != nil { + log.Printf("Failed to save user: %v", err) + http.Error(w, "Failed to save user", http.StatusInternalServerError) + return + } + + // Begin registration + options, sessionData, err := s.webAuthn.BeginRegistration( + user, + // You can customize options here + ) + if err != nil { + log.Printf("Failed to begin registration: %v", err) + http.Error(w, "Failed to begin registration", http.StatusInternalServerError) + return + } + + // Store session data + s.sessionStore[userID] = sessionData + + // Create response with both options and userID + response := struct { + PublicKey *protocol.CredentialCreation `json:"publicKey"` + UserID string `json:"userID"` + }{ + PublicKey: options, + UserID: userID, + } + + // Return options to client + jsonResponse(w, response) +} + +// FinishRegistration completes the registration process +func (s *Server) FinishRegistration(w http.ResponseWriter, r *http.Request) { + // First, get the userID from query parameter + userID := r.URL.Query().Get("userID") + if userID == "" { + userID = r.Header.Get("X-User-ID") // Fallback to header + } + + if userID == "" { + log.Printf("UserID not provided") + http.Error(w, "UserID not provided", http.StatusBadRequest) + return + } + + user, err := s.db.GetUserByID(r.Context(), userID) + if err != nil || user == nil { + log.Printf("User not found: %v", err) + http.Error(w, "User not found", http.StatusNotFound) + return + } + + // Get session data + sessionData, ok := s.sessionStore[userID] + if !ok { + log.Printf("Session data not found for user ID: %s", userID) + http.Error(w, "Session data not found", http.StatusBadRequest) + return + } + + credential, err := s.webAuthn.FinishRegistration(user, *sessionData, r) + if err != nil { + log.Printf("Failed to finish registration: %v", err) + http.Error(w, "Failed to finish registration", http.StatusBadRequest) + return + } + + log.Printf("Credential details: %+v", credential) + + // Extract backup flags from the credential's Flags field + backupEligible := false + // backupState := true + + // Save the credential with the flags + cred := &models.Credential{ + UserID: user.ID, + PublicKey: credential.PublicKey, + CredentialID: credential.ID, + SignCount: credential.Authenticator.SignCount, + AAGUID: credential.Authenticator.AAGUID, + CloneWarning: credential.Authenticator.CloneWarning, + Attachment: credential.Authenticator.Attachment, + BackupEligible: backupEligible, + // BackupState: backupState, + } + + log.Printf("Saving credential with flags - BackupEligible: %v", + backupEligible) + + err = s.db.SaveCredential(r.Context(), cred) + if err != nil { + log.Printf("Failed to save credential: %v", err) + http.Error(w, "Failed to save credential", http.StatusInternalServerError) + return + } + + // Log successful registration + log.Printf("Successfully registered credential for user %s", user.ID) + + // Clean up session data + delete(s.sessionStore, userID) + + jsonResponse(w, map[string]string{"status": "ok"}) +} + +func (s *Server) BeginLogin(w http.ResponseWriter, r *http.Request) { + var req struct { + Username string `json:"username"` + } + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil || req.Username == "" { + log.Printf("Invalid request payload: %v", err) + http.Error(w, "Invalid request payload", http.StatusBadRequest) + return + } + + user, err := s.db.GetUserByName(r.Context(), req.Username) + if err != nil || user == nil { + log.Printf("User not found: %v", err) + http.Error(w, "User not found", http.StatusNotFound) + return + } + + options, sessionData, err := s.webAuthn.BeginLogin(user) + if err != nil { + log.Printf("Failed to begin login: %v", err) + http.Error(w, "Failed to begin login", http.StatusInternalServerError) + return + } + + s.sessionStore[user.ID] = sessionData + + // Create response with both options and userID + response := struct { + PublicKey *protocol.CredentialAssertion `json:"publicKey"` + UserID string `json:"userID"` + }{ + PublicKey: options, + UserID: user.ID, + } + + jsonResponse(w, response) +} + +// FinishLogin completes the login process +func (s *Server) FinishLogin(w http.ResponseWriter, r *http.Request) { + // Get userID from query parameter + userID := r.URL.Query().Get("userID") + if userID == "" { + log.Printf("UserID not provided") + http.Error(w, "UserID not provided", http.StatusBadRequest) + return + } + + user, err := s.db.GetUserByID(r.Context(), userID) + if err != nil || user == nil { + log.Printf("User not found: %v", err) + http.Error(w, "User not found", http.StatusNotFound) + return + } + + sessionData, ok := s.sessionStore[user.ID] + if !ok { + log.Printf("Session data not found for user ID: %s", user.ID) + http.Error(w, "Session data not found", http.StatusBadRequest) + return + } + + credential, err := s.webAuthn.FinishLogin(user, *sessionData, r) + if err != nil { + log.Printf("Login failed with detailed error: %+v", err) + http.Error(w, "Failed to finish login", http.StatusUnauthorized) + return + } + + // Log successful validation + log.Printf("Successfully validated credential for user %s", user.ID) + + // Update credential's sign count and backup state + err = s.db.UpdateCredentialSignCount(r.Context(), credential.ID, credential.Authenticator.SignCount) + if err != nil { + log.Printf("Failed to update credential: %v", err) + http.Error(w, "Failed to update credential", http.StatusInternalServerError) + return + } + + delete(s.sessionStore, user.ID) + + // Create session for authenticated user + sessionID := uuid.New().String() + s.sessionMutex.Lock() + s.userSessions[sessionID] = user.ID + s.sessionMutex.Unlock() + + // Set cookie with session ID + http.SetCookie(w, &http.Cookie{ + Name: "sessionID", + Value: sessionID, + Path: "/", + Expires: time.Now().Add(24 * time.Hour), + HttpOnly: true, + Secure: false, // Set to true if using HTTPS + SameSite: http.SameSiteLaxMode, + }) + + jsonResponse(w, map[string]string{"status": "ok"}) +} + +// GetCurrentUser returns the current user's information if authenticated +func (s *Server) GetCurrentUser(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserFromSession(r) + if err != nil || user == nil { + http.Error(w, "Not authenticated", http.StatusUnauthorized) + return + } + + // Optionally, you may want to omit sensitive information + responseUser := struct { + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + }{ + ID: user.ID, + Name: user.Name, + DisplayName: user.DisplayName, + } + + jsonResponse(w, responseUser) +} + +func jsonResponse(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +}
M core/internal/server/routes.gocore/internal/server/routes.go

@@ -7,13 +7,34 @@ "net/http"

"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" ) func (s *Server) RegisterRoutes() http.Handler { r := chi.NewRouter() r.Use(middleware.Logger) + // Add CORS middleware + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"http://localhost:3000"}, // Your frontend origin + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + AllowCredentials: true, // Important for cookies + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + r.Get("/", s.HelloWorldHandler) + + // Registration endpoints + r.Post("/register/begin", s.BeginRegistration) + r.Post("/register/finish", s.FinishRegistration) + + // Login endpoints + r.Post("/login/begin", s.BeginLogin) + r.Post("/login/finish", s.FinishLogin) + + // Protected endpoint + r.With(s.AuthMiddleware).Get("/me", s.GetCurrentUser) return r }
M core/internal/server/server.gocore/internal/server/server.go

@@ -1,29 +1,61 @@

package server import ( + "context" "fmt" + "log" "net/http" "os" "strconv" + "sync" "time" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" _ "github.com/joho/godotenv/autoload" "core/internal/database" + "core/internal/models" ) type Server struct { port int + db database.Service + + webAuthn *webauthn.WebAuthn + sessionStore map[string]*webauthn.SessionData - db database.Service + userSessions map[string]string + sessionMutex sync.RWMutex } func NewServer() *http.Server { port, _ := strconv.Atoi(os.Getenv("PORT")) - NewServer := &Server{ - port: port, + dbService := database.New() - db: database.New(), + // Initialize WebAuthn with correct config + wconfig := &webauthn.Config{ + RPDisplayName: "My App", + RPID: "localhost", + RPOrigins: []string{"http://localhost:3000"}, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + RequireResidentKey: &[]bool{false}[0], + UserVerification: protocol.VerificationPreferred, + }, + AttestationPreference: protocol.PreferNoAttestation, + Debug: true, // Enable debug logging + } + webAuthn, err := webauthn.New(wconfig) + if err != nil { + log.Fatalf("Failed to create WebAuthn from config: %v", err) + } + + NewServer := &Server{ + port: port, + db: dbService, + webAuthn: webAuthn, + sessionStore: make(map[string]*webauthn.SessionData), + userSessions: make(map[string]string), } // Declare Server config

@@ -37,3 +69,37 @@ }

return server } + +func (s *Server) getUserFromSession(r *http.Request) (*models.User, error) { + cookie, err := r.Cookie("sessionID") + if err != nil { + return nil, fmt.Errorf("No session cookie") + } + sessionID := cookie.Value + + s.sessionMutex.RLock() + userID, ok := s.userSessions[sessionID] + s.sessionMutex.RUnlock() + if !ok { + return nil, fmt.Errorf("Invalid session ID") + } + + user, err := s.db.GetUserByID(r.Context(), userID) + if err != nil || user == nil { + return nil, fmt.Errorf("User not found") + } + return user, nil +} + +func (s *Server) AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserFromSession(r) + if err != nil || user == nil { + http.Error(w, "Not authenticated", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), "user", user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +}