Skip to content

Commit

Permalink
add user Login and lambda authorizer
Browse files Browse the repository at this point in the history
  • Loading branch information
Dieg0Code committed Aug 16, 2024
1 parent 7ac64dc commit 2a3e77d
Show file tree
Hide file tree
Showing 31 changed files with 515 additions and 19 deletions.
63 changes: 63 additions & 0 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ jobs:
test-and-build-api-users:
name: Test and Build API Users
runs-on: ubuntu-latest
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -170,6 +172,62 @@ jobs:
name: api_users_lambda
path: ./api-users/api_users_lambda.zip

test-and-build-authorizer:
name: Test and Build Authorizer
runs-on: ubuntu-latest
needs: test-and-build-api-users
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.22.4'

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install dependencies
working-directory: ./authorizer
run: go mod download

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6.0.1
with:
working-directory: .
args: --out-format colored-line-number ./authorizer/...

- name: Run tests
working-directory: ./authorizer
run: go test -coverprofile=coverage.out ./...

- name: Upload coverage to codecov
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: Build Authorizer binary
working-directory: ./authorizer
run: GOOS=linux GOARCH=amd64 go build -o bootstrap main.go

- name: Zip Authorizer binary
working-directory: ./authorizer
run: zip authorizer_lambda.zip bootstrap

- name: Upload Authorizer artifact
uses: actions/upload-artifact@v2
with:
name: authorizer_lambda
path: ./authorizer/authorizer_lambda.zip

deploy:
name: Deploy
runs-on: ubuntu-latest
Expand All @@ -195,6 +253,11 @@ jobs:
with:
name: api_users_lambda
path: ./terraform
- name: Download Authorizer artifact
uses: actions/download-artifact@v2
with:
name: authorizer_lambda
path: ./terraform

- name: Set up Terraform
uses: hashicorp/setup-terraform@v1
Expand Down
1 change: 1 addition & 0 deletions api-users/controllers/user_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ type UserController interface {
RegisterUser(c *gin.Context)
GetAllUsers(c *gin.Context)
GetUserByID(c *gin.Context)
LogInUser(c *gin.Context)
}
43 changes: 43 additions & 0 deletions api-users/controllers/user_controller_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,49 @@ type UserControllerImpl struct {
userService services.UserService
}

// LogInUser implements UserController.
func (u *UserControllerImpl) LogInUser(c *gin.Context) {
loginRequest := request.LogInUserRequest{}

err := c.ShouldBindJSON(&loginRequest)
if err != nil {
logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error binding JSON")
errorResponse := response.BaseResponse{
Code: 400,
Status: "error",
Message: "Invalid request body",
Data: nil,
}

c.JSON(400, errorResponse)
return
}

loginResponse, err := u.userService.LogInUser(loginRequest)
if err != nil {
logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error logging in user")
errorResponse := response.BaseResponse{
Code: 500,
Status: "error",
Message: "Error logging in user",
Data: nil,
}

c.JSON(500, errorResponse)
return
}

c.Header("Authorization", fmt.Sprintf("Bearer %s", loginResponse.Token))
webResponse := response.BaseResponse{
Code: 200,
Status: "success",
Message: "User logged in successfully",
Data: loginResponse,
}

c.JSON(200, webResponse)
}

// GetAllUsers implements UserController.
func (u *UserControllerImpl) GetAllUsers(c *gin.Context) {
users, err := u.userService.GetAllUsers()
Expand Down
2 changes: 2 additions & 0 deletions api-users/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/dieg0code/api-users

go 1.22.4

require github.com/golang-jwt/jwt/v5 v5.2.1
2 changes: 2 additions & 0 deletions api-users/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
3 changes: 2 additions & 1 deletion api-users/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ func init() {

validator := validator.New()
passwordHaher := utils.NewPasswordHasher()
jwtUtils := utils.NewJWTUtils()

// Instance Service
userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher)
userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher, jwtUtils)

// Instance controller
userController := controllers.NewUserControllerImpl(userService)
Expand Down
1 change: 1 addition & 0 deletions api-users/repository/user_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ type UserRepository interface {
GetAll() ([]models.User, error)
GetByID(id string) (models.User, error)
Create(user models.User) (models.User, error)
GetByEmail(email string) (models.User, error)
}
33 changes: 33 additions & 0 deletions api-users/repository/user_repository_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ type UserRepositoryImpl struct {
tableName string
}

// GetByEmail implements UserRepository.
func (u *UserRepositoryImpl) GetByEmail(email string) (models.User, error) {
input := &dynamodb.QueryInput{
TableName: &u.tableName,
IndexName: aws.String("EmailIndex"),
KeyConditionExpression: aws.String("Email = :email"),
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":email": {
S: aws.String(email),
},
},
}

result, err := u.db.Query(input)
if err != nil {
logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error getting user")
return models.User{}, errors.New("error getting user")
}

if len(result.Items) == 0 {
return models.User{}, errors.New("user not found")
}

var user models.User
err = dynamodbattribute.UnmarshalMap(result.Items[0], &user)
if err != nil {
logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error unmarshalling user")
return models.User{}, errors.New("error getting user")
}

return user, nil
}

// Create implements UserRepository.
func (u *UserRepositoryImpl) Create(user models.User) (models.User, error) {
input := &dynamodb.PutItemInput{
Expand Down
1 change: 1 addition & 0 deletions api-users/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (r *Router) InitRoutes() *Router {
userRoute.GET("", r.UserController.GetAllUsers)
userRoute.GET("/:userID", r.UserController.GetUserByID)
userRoute.POST("", r.UserController.RegisterUser)
userRoute.POST("/login", r.UserController.LogInUser)
}
}

Expand Down
1 change: 1 addition & 0 deletions api-users/services/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ type UserService interface {
RegisterUser(createUserReq request.CreateUserRequest) (models.User, error)
GetAllUsers() ([]response.UserResponse, error)
GetUserByID(id string) (response.UserResponse, error)
LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error)
}
43 changes: 42 additions & 1 deletion api-users/services/user_service_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,46 @@ type UserServiceImpl struct {
UserRepository repository.UserRepository
Validator *validator.Validate
PasswordHasher utils.PasswordHasher
JWTUtils utils.JWTUtils
}

// LogInUser implements UserService.
func (u *UserServiceImpl) LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error) {
err := u.Validator.Struct(logInUserReq)
if err != nil {
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user request")
return response.LogInUserResponse{}, err
}

user, err := u.UserRepository.GetByEmail(logInUserReq.Email)
if err != nil {
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error getting user by email")
return response.LogInUserResponse{}, err
}

err = u.PasswordHasher.ComparePassword(user.Password, logInUserReq.Password)
if err != nil {
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error comparing password")
return response.LogInUserResponse{}, errors.New("invalid password")
}

token, err := u.JWTUtils.GenerateToken(user.UserID)
if err != nil {
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error generating token")
return response.LogInUserResponse{}, err
}

logInUserResponse := response.LogInUserResponse{
Token: token,
}

err = u.Validator.Struct(logInUserResponse)
if err != nil {
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user response")
return response.LogInUserResponse{}, err
}

return logInUserResponse, nil
}

// GetAllUsers implements UserService.
Expand Down Expand Up @@ -105,10 +145,11 @@ func (u *UserServiceImpl) RegisterUser(createUserReq request.CreateUserRequest)
return user, nil
}

func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher) UserService {
func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher, jwtUtils utils.JWTUtils) UserService {
return &UserServiceImpl{
UserRepository: userRepository,
Validator: validator,
PasswordHasher: passwordHaher,
JWTUtils: jwtUtils,
}
}
5 changes: 5 additions & 0 deletions api-users/utils/jwt_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package utils

type JWTUtils interface {
GenerateToken(userID string) (string, error)
}
33 changes: 33 additions & 0 deletions api-users/utils/jwt_utils_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package utils

import (
"os"
"time"

"github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

type JWTUtilsImpl struct{}

// GenerateToken implements JWTUtils.
func (j *JWTUtilsImpl) GenerateToken(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

signedToken, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}

return signedToken, nil
}

func NewJWTUtils() JWTUtils {
return &JWTUtilsImpl{}
}
1 change: 1 addition & 0 deletions api-users/utils/password_hasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package utils

type PasswordHasher interface {
HashPassword(password string) (string, error)
ComparePassword(hashedPassword, password string) error
}
5 changes: 5 additions & 0 deletions api-users/utils/password_hasher_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (

type PasswordHasherImpl struct{}

// ComparePassword implements PasswordHasher.
func (p *PasswordHasherImpl) ComparePassword(hashedPassword string, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

// HashPassword implements PasswordHasher.
func (p *PasswordHasherImpl) HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
Expand Down
7 changes: 7 additions & 0 deletions authorizer/auth/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package auth

import "github.com/golang-jwt/jwt/v5"

type JWTValidator interface {
ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error)
}
38 changes: 38 additions & 0 deletions authorizer/auth/jwt_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package auth

import (
"errors"

"github.com/golang-jwt/jwt/v5"
)

type jwtValidator struct{}

// ValidateToken implements JWTValidator.
func (v *jwtValidator) ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, errors.New("unexpected signing method")
}
return secret, nil
})

if err != nil {
return nil, err
}

if !token.Valid {
return nil, errors.New("invalid token")
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid token claims")
}

return claims, nil
}

func NewJWTValidator() JWTValidator {
return &jwtValidator{}
}
7 changes: 7 additions & 0 deletions authorizer/aws/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package aws

import "github.com/aws/aws-lambda-go/events"

type Policy interface {
GeneratePolicy(principalID, effect, resource string) events.APIGatewayCustomAuthorizerResponse
}
Loading

0 comments on commit 2a3e77d

Please sign in to comment.