From 2e82b155df05d140240ac25b3654ac180352c1d0 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sat, 27 Jun 2026 22:41:43 +0530 Subject: [PATCH 1/2] feat: implement access/refresh tokens on auth --- internal/handlers/login.go | 48 ++++++++++++++++++++++++ internal/handlers/{goth.go => signup.go} | 39 ------------------- internal/services/token.go | 5 +++ 3 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 internal/handlers/login.go rename internal/handlers/{goth.go => signup.go} (63%) create mode 100644 internal/services/token.go diff --git a/internal/handlers/login.go b/internal/handlers/login.go new file mode 100644 index 0000000..c674ebe --- /dev/null +++ b/internal/handlers/login.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "context" + + "github.com/ayush00git/goth/grpc/goth" + "golang.org/x/crypto/bcrypt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (g *GothServer) Login(ctx context.Context, req *goth.LoginRequest) (*goth.LoginResponse, error) { + // inputs + email := req.Email + password := req.Password + + // validate the inputs + if email == "" || password == "" { + return nil, status.Error(codes.InvalidArgument, "email and password are required fields.") + } + + // email lookup into db. + var user RequestedUser + err := g.db.QueryRow( + ctx, + `SELECT id, email, password_hash + FROM users + WHERE $1 = email`, email, + ).Scan(&user.id, &user.email, &user.password) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "user not found") + } + + // match the password + err = bcrypt.CompareHashAndPassword([]byte(user.password), []byte(password)) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "credentials do not match.") + } + + // just placeholders for now + return &goth.LoginResponse{ + AccessToken: "need-to-be-set", + RefreshToken: "need-to-be-set", + MfaRequired: true, + MfaSessionToken: "need-to-be-set", + MfaType: 0, + }, nil +} diff --git a/internal/handlers/goth.go b/internal/handlers/signup.go similarity index 63% rename from internal/handlers/goth.go rename to internal/handlers/signup.go index 97cb80e..8e06b1d 100644 --- a/internal/handlers/goth.go +++ b/internal/handlers/signup.go @@ -73,42 +73,3 @@ func (g *GothServer) Signup(ctx context.Context, req *goth.SignupRequest) (*goth EmailVerified: false, }, nil } - - -func (g *GothServer) Login(ctx context.Context, req *goth.LoginRequest) (*goth.LoginResponse, error) { - // inputs - email := req.Email - password := req.Password - - // validate the inputs - if email == "" || password == "" { - return nil, status.Error(codes.InvalidArgument, "email and password are required fields.") - } - - // email lookup into db. - var user RequestedUser - err := g.db.QueryRow( - ctx, - `SELECT id, email, password_hash - FROM users - WHERE $1 = email`, email, - ).Scan(&user.id, &user.email, &user.password) - if err != nil { - return nil, status.Error(codes.Unauthenticated, "user not found") - } - - // match the password - err = bcrypt.CompareHashAndPassword([]byte(user.password), []byte(password)) - if err != nil { - return nil, status.Error(codes.Unauthenticated, "credentials do not match.") - } - - // just placeholders for now - return &goth.LoginResponse{ - AccessToken: "need-to-be-set", - RefreshToken: "need-to-be-set", - MfaRequired: true, - MfaSessionToken: "need-to-be-set", - MfaType: 0, - }, nil -} diff --git a/internal/services/token.go b/internal/services/token.go new file mode 100644 index 0000000..a8fefaa --- /dev/null +++ b/internal/services/token.go @@ -0,0 +1,5 @@ +package services + +import () + +func () From c9aaff90e8bbf194a52eaf12c819c9be48ac4b94 Mon Sep 17 00:00:00 2001 From: ayush00git Date: Sun, 28 Jun 2026 22:44:43 +0530 Subject: [PATCH 2/2] feat: implemented access/refresh tokens validate/gen methods --- go.mod | 1 + go.sum | 2 + internal/services/token.go | 86 +++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f602a34..e9d4901 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/apache/fory/go/fory v1.1.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.10.0 golang.org/x/crypto v0.48.0 google.golang.org/grpc v1.81.1 diff --git a/go.sum b/go.sum index 29db9b1..3a274ea 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/internal/services/token.go b/internal/services/token.go index a8fefaa..39f3126 100644 --- a/internal/services/token.go +++ b/internal/services/token.go @@ -1,5 +1,87 @@ package services -import () +import ( + "errors" + "time" -func () + "github.com/golang-jwt/jwt/v5" +) + +// jwt payload claims +type Claims struct { + UserID string `json:"user_id"` + Email string `json:"email"` + DeviceFingerprint string `json:"device_fingerprint,omitempty"` + jwt.RegisteredClaims +} + +// Helper function which generates an access token which helps identifying +// authorized users at every protected handlers/rpc services. +func GenerateAccessToken (userId, email string) (string, error) { + // define the claims/payload including the registered claims. + claims := Claims{ + UserID: userId, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), // expiration time - 15 min + }, + } + // "token" object with "NewWithClaims" defining the signing + // method and claims contained in the object. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // sign the token string. + tokenString, err := token.SignedString([]byte("jwuweufwfweif")) // move it to environment variabls after + if err != nil { + return "", err + } + + return tokenString, nil +} + +// Helper function which validates the user's authorization via his access token +// which allows user's access to protected rpc services. +func ValidateAccessToken(tokenString string) (*Claims, error) { + secretKey := []byte("jwuweufwfweif") // move to env variables after + // parse the jwt token string. + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, + func(token *jwt.Token) (interface{}, error) { + return secretKey, nil + }, + ) + if err != nil { + return nil, err + } + + // validate the token. + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("Invalid Token") +} + +// Helper function which generates a refresh token which helps re-generating the +// access tokens, this token is stored in a database and revoked after a specific time. +func GenerateRefreshToken(userId, email, fingerprint string) (string, error) { + // build the claims object. + claims := Claims{ + UserID: userId, + Email: email, + DeviceFingerprint: fingerprint, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // expiration time - 24 hours + }, + } + + // building the token object with "NewWithClaims" + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + tokenString, err := token.SignedString([]byte("euiy348bcues")) // move it to env vars later. + if err != nil { + return "", err + } + + return tokenString, nil +}