diff --git a/go.mod b/go.mod index 3f74173..50d1353 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/ethereum/go-ethereum v1.16.3 + github.com/gofiber/fiber/v2 v2.52.9 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/spf13/viper v1.20.1 @@ -12,6 +13,7 @@ require ( ) require ( + github.com/andybalholm/brotli v1.1.0 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect @@ -28,7 +30,12 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -36,6 +43,9 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.14 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.36.0 // indirect diff --git a/go.sum b/go.sum index 29fef1b..cd661d5 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -37,6 +39,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= +github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -61,6 +65,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -71,8 +77,13 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= @@ -114,6 +125,12 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -122,6 +139,8 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= diff --git a/internal/api/dto/.gitkeep b/internal/api/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/api/dto/auth.go b/internal/api/dto/auth.go new file mode 100644 index 0000000..c1470ef --- /dev/null +++ b/internal/api/dto/auth.go @@ -0,0 +1,27 @@ +package dto + +type ChallengeRequest struct { + PubKey string `json:"pubKey" validate:"required,eth_pubkey"` +} + +type ChallengeResponse struct { + Message string `json:"message"` + TimeStamp string `json:"timeStamp"` + ExpiresAt string `json:"expiresAt"` +} + +type AuthenticateRequest struct { + PubKey string `json:"pubKey" validate:"required,eth_pubkey"` + Signature string `json:"signature" validate:"required,eth_signature"` + Message string `json:"message" validate:"required,uuid"` +} + +type AuthenticateResponse struct { + AuthorizationToken string `json:"authorizationToken"` + RefreshToken string `json:"refreshToken"` + ExpiresAt int64 `json:"expiresAt"` +} + +type RefreshTokenRequest struct { + RefreshToken string `json:"refreshToken" validate:"required,jwt"` +} diff --git a/internal/api/handlers/.gitkeep b/internal/api/handlers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/api/http/handlers/auth_handler.go b/internal/api/http/handlers/auth_handler.go new file mode 100644 index 0000000..dfbbccb --- /dev/null +++ b/internal/api/http/handlers/auth_handler.go @@ -0,0 +1,90 @@ +package http + +import ( + "backend/internal/api/dto" + "backend/internal/domain" + "backend/internal/usecase" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +type AuthHandler struct { + authService usecase.AuthService +} + +func NewAuthHandler(authService *usecase.AuthService) *AuthHandler { + return &AuthHandler{ + authService: *authService, + } +} + +func (h *AuthHandler) GenerateChallenge(c *fiber.Ctx) error { + var req dto.ChallengeRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid request body", + }) + } + challenge, err := h.authService.GenerateChallenge(c.Context(), req.PubKey) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "failed to generate challenge", + }) + } + return c.Status(fiber.StatusOK).JSON( + dto.ChallengeResponse{ + Message: challenge.Message.String(), + TimeStamp: challenge.TimeStamp.String(), + ExpiresAt: challenge.ExpiresAt.String(), + }, + ) +} + +func (h *AuthHandler) Authenticate(c *fiber.Ctx) error { + var req dto.AuthenticateRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid request body", + }) + } + + messageUUID, err := uuid.Parse(req.Message) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "invalid message format", + }) + } + + challenge := &domain.Challenge{ + Message: messageUUID, + TimeStamp: time.Now().UTC(), + ExpiresAt: time.Now().Add(5 * time.Minute), + } + + clientIP := c.IP() + userAgent := c.Get("User-Agent") + + userToken, err := h.authService.Authenticate( + c.Context(), + req.PubKey, + req.Signature, + challenge, + // add chainID to cfg + 1, + clientIP, + userAgent, + ) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(dto.AuthenticateResponse{ + AuthorizationToken: userToken.AuthorizationToken, + RefreshToken: userToken.RefreshToken, + ExpiresAt: userToken.ExpiresAt, + }) +} diff --git a/internal/api/http/middlewares/auth.go b/internal/api/http/middlewares/auth.go new file mode 100644 index 0000000..838cadf --- /dev/null +++ b/internal/api/http/middlewares/auth.go @@ -0,0 +1,37 @@ +package middlewares + +import ( + "backend/pkg/jwt" + "strings" + + "github.com/gofiber/fiber/v2" +) + +const userClaimsKey = "User-Claims" + +func JWTAuthMiddleware(secret []byte) fiber.Handler { + return func(c *fiber.Ctx) error { + authHeader := c.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "missing or invalid Authorization header", + }) + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + claims, err := jwt.ParseToken(tokenString, secret) + if err != nil || claims == nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "invalid or expired token", + }) + } + + c.Locals(userClaimsKey, claims) + return c.Next() + } +} + +func GetUserClaims(c *fiber.Ctx) *jwt.UserClaims { + claims, _ := c.Locals(userClaimsKey).(*jwt.UserClaims) + return claims +} diff --git a/internal/api/http/setup.go b/internal/api/http/setup.go new file mode 100644 index 0000000..8610cb5 --- /dev/null +++ b/internal/api/http/setup.go @@ -0,0 +1,27 @@ +package http + +import ( + "backend/config" + "backend/internal/app" + "fmt" + "log" + + "github.com/gofiber/fiber/v2" +) + +func Run(cfg config.Server, app *app.AppContainer) { + fiberApp := fiber.New() + api := fiberApp.Group("/api") + // register routes here + registerPublicRoutes(api, app) + + log.Fatal(fiberApp.Listen(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))) +} + +func registerPublicRoutes(router fiber.Router, app *app.AppContainer) { + authgroup := router.Group("/auth") + + //TODO: implement handlers + authgroup.Post("/challenge") + authgroup.Post("/authenticate") +} diff --git a/internal/api/middlewares/.gitkeep b/internal/api/middlewares/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/app/app.go b/internal/app/app.go index 4879f7a..9436396 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1 +1,14 @@ package app + +import ( + "backend/config" + "backend/internal/usecase" + + "gorm.io/gorm" +) + +type AppContainer struct { + cfg config.Config + dbConn *gorm.DB + authService usecase.AuthService +} diff --git a/internal/usecase/auth_service.go b/internal/usecase/auth_service.go index fb45ba9..6d24978 100644 --- a/internal/usecase/auth_service.go +++ b/internal/usecase/auth_service.go @@ -17,7 +17,7 @@ import ( type AuthService interface { GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) - Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint) (*UserToken, error) + Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint, ipAddress, userAgent string) (*UserToken, error) } type authService struct { @@ -61,7 +61,7 @@ func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*do return challenge, nil } -func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint) (*UserToken, error) { +func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, challenge *domain.Challenge, chainID uint, ipAddress, userAgent string) (*UserToken, error) { _, err := common.IsValidPublicKey(pubKey) if err != nil { return nil, fmt.Errorf("invalid public key: %w", err)