Skip to content

Commit 2a3e77d

Browse files
committed
add user Login and lambda authorizer
1 parent 7ac64dc commit 2a3e77d

31 files changed

+515
-19
lines changed

.github/workflows/cicd.yaml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ jobs:
120120
test-and-build-api-users:
121121
name: Test and Build API Users
122122
runs-on: ubuntu-latest
123+
env:
124+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
123125
steps:
124126
- name: Checkout code
125127
uses: actions/checkout@v4
@@ -170,6 +172,62 @@ jobs:
170172
name: api_users_lambda
171173
path: ./api-users/api_users_lambda.zip
172174

175+
test-and-build-authorizer:
176+
name: Test and Build Authorizer
177+
runs-on: ubuntu-latest
178+
needs: test-and-build-api-users
179+
env:
180+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
181+
steps:
182+
- name: Checkout code
183+
uses: actions/checkout@v4
184+
185+
- name: Set up Go
186+
uses: actions/setup-go@v2
187+
with:
188+
go-version: '1.22.4'
189+
190+
- name: Cache dependencies
191+
uses: actions/cache@v2
192+
with:
193+
path: ~/go/pkg/mod
194+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
195+
restore-keys: |
196+
${{ runner.os }}-go-
197+
198+
- name: Install dependencies
199+
working-directory: ./authorizer
200+
run: go mod download
201+
202+
- name: Run golangci-lint
203+
uses: golangci/golangci-lint-action@v6.0.1
204+
with:
205+
working-directory: .
206+
args: --out-format colored-line-number ./authorizer/...
207+
208+
- name: Run tests
209+
working-directory: ./authorizer
210+
run: go test -coverprofile=coverage.out ./...
211+
212+
- name: Upload coverage to codecov
213+
uses: codecov/codecov-action@v2
214+
with:
215+
token: ${{ secrets.CODECOV_TOKEN }}
216+
217+
- name: Build Authorizer binary
218+
working-directory: ./authorizer
219+
run: GOOS=linux GOARCH=amd64 go build -o bootstrap main.go
220+
221+
- name: Zip Authorizer binary
222+
working-directory: ./authorizer
223+
run: zip authorizer_lambda.zip bootstrap
224+
225+
- name: Upload Authorizer artifact
226+
uses: actions/upload-artifact@v2
227+
with:
228+
name: authorizer_lambda
229+
path: ./authorizer/authorizer_lambda.zip
230+
173231
deploy:
174232
name: Deploy
175233
runs-on: ubuntu-latest
@@ -195,6 +253,11 @@ jobs:
195253
with:
196254
name: api_users_lambda
197255
path: ./terraform
256+
- name: Download Authorizer artifact
257+
uses: actions/download-artifact@v2
258+
with:
259+
name: authorizer_lambda
260+
path: ./terraform
198261

199262
- name: Set up Terraform
200263
uses: hashicorp/setup-terraform@v1

api-users/controllers/user_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ type UserController interface {
66
RegisterUser(c *gin.Context)
77
GetAllUsers(c *gin.Context)
88
GetUserByID(c *gin.Context)
9+
LogInUser(c *gin.Context)
910
}

api-users/controllers/user_controller_impl.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,49 @@ type UserControllerImpl struct {
1414
userService services.UserService
1515
}
1616

17+
// LogInUser implements UserController.
18+
func (u *UserControllerImpl) LogInUser(c *gin.Context) {
19+
loginRequest := request.LogInUserRequest{}
20+
21+
err := c.ShouldBindJSON(&loginRequest)
22+
if err != nil {
23+
logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error binding JSON")
24+
errorResponse := response.BaseResponse{
25+
Code: 400,
26+
Status: "error",
27+
Message: "Invalid request body",
28+
Data: nil,
29+
}
30+
31+
c.JSON(400, errorResponse)
32+
return
33+
}
34+
35+
loginResponse, err := u.userService.LogInUser(loginRequest)
36+
if err != nil {
37+
logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error logging in user")
38+
errorResponse := response.BaseResponse{
39+
Code: 500,
40+
Status: "error",
41+
Message: "Error logging in user",
42+
Data: nil,
43+
}
44+
45+
c.JSON(500, errorResponse)
46+
return
47+
}
48+
49+
c.Header("Authorization", fmt.Sprintf("Bearer %s", loginResponse.Token))
50+
webResponse := response.BaseResponse{
51+
Code: 200,
52+
Status: "success",
53+
Message: "User logged in successfully",
54+
Data: loginResponse,
55+
}
56+
57+
c.JSON(200, webResponse)
58+
}
59+
1760
// GetAllUsers implements UserController.
1861
func (u *UserControllerImpl) GetAllUsers(c *gin.Context) {
1962
users, err := u.userService.GetAllUsers()

api-users/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/dieg0code/api-users
22

33
go 1.22.4
4+
5+
require github.com/golang-jwt/jwt/v5 v5.2.1

api-users/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
2+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=

api-users/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ func init() {
3131

3232
validator := validator.New()
3333
passwordHaher := utils.NewPasswordHasher()
34+
jwtUtils := utils.NewJWTUtils()
3435

3536
// Instance Service
36-
userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher)
37+
userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher, jwtUtils)
3738

3839
// Instance controller
3940
userController := controllers.NewUserControllerImpl(userService)

api-users/repository/user_repository.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ type UserRepository interface {
66
GetAll() ([]models.User, error)
77
GetByID(id string) (models.User, error)
88
Create(user models.User) (models.User, error)
9+
GetByEmail(email string) (models.User, error)
910
}

api-users/repository/user_repository_impl.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@ type UserRepositoryImpl struct {
1616
tableName string
1717
}
1818

19+
// GetByEmail implements UserRepository.
20+
func (u *UserRepositoryImpl) GetByEmail(email string) (models.User, error) {
21+
input := &dynamodb.QueryInput{
22+
TableName: &u.tableName,
23+
IndexName: aws.String("EmailIndex"),
24+
KeyConditionExpression: aws.String("Email = :email"),
25+
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
26+
":email": {
27+
S: aws.String(email),
28+
},
29+
},
30+
}
31+
32+
result, err := u.db.Query(input)
33+
if err != nil {
34+
logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error getting user")
35+
return models.User{}, errors.New("error getting user")
36+
}
37+
38+
if len(result.Items) == 0 {
39+
return models.User{}, errors.New("user not found")
40+
}
41+
42+
var user models.User
43+
err = dynamodbattribute.UnmarshalMap(result.Items[0], &user)
44+
if err != nil {
45+
logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error unmarshalling user")
46+
return models.User{}, errors.New("error getting user")
47+
}
48+
49+
return user, nil
50+
}
51+
1952
// Create implements UserRepository.
2053
func (u *UserRepositoryImpl) Create(user models.User) (models.User, error) {
2154
input := &dynamodb.PutItemInput{

api-users/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func (r *Router) InitRoutes() *Router {
3838
userRoute.GET("", r.UserController.GetAllUsers)
3939
userRoute.GET("/:userID", r.UserController.GetUserByID)
4040
userRoute.POST("", r.UserController.RegisterUser)
41+
userRoute.POST("/login", r.UserController.LogInUser)
4142
}
4243
}
4344

api-users/services/user_service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type UserService interface {
1010
RegisterUser(createUserReq request.CreateUserRequest) (models.User, error)
1111
GetAllUsers() ([]response.UserResponse, error)
1212
GetUserByID(id string) (response.UserResponse, error)
13+
LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error)
1314
}

api-users/services/user_service_impl.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ type UserServiceImpl struct {
1717
UserRepository repository.UserRepository
1818
Validator *validator.Validate
1919
PasswordHasher utils.PasswordHasher
20+
JWTUtils utils.JWTUtils
21+
}
22+
23+
// LogInUser implements UserService.
24+
func (u *UserServiceImpl) LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error) {
25+
err := u.Validator.Struct(logInUserReq)
26+
if err != nil {
27+
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user request")
28+
return response.LogInUserResponse{}, err
29+
}
30+
31+
user, err := u.UserRepository.GetByEmail(logInUserReq.Email)
32+
if err != nil {
33+
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error getting user by email")
34+
return response.LogInUserResponse{}, err
35+
}
36+
37+
err = u.PasswordHasher.ComparePassword(user.Password, logInUserReq.Password)
38+
if err != nil {
39+
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error comparing password")
40+
return response.LogInUserResponse{}, errors.New("invalid password")
41+
}
42+
43+
token, err := u.JWTUtils.GenerateToken(user.UserID)
44+
if err != nil {
45+
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error generating token")
46+
return response.LogInUserResponse{}, err
47+
}
48+
49+
logInUserResponse := response.LogInUserResponse{
50+
Token: token,
51+
}
52+
53+
err = u.Validator.Struct(logInUserResponse)
54+
if err != nil {
55+
logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user response")
56+
return response.LogInUserResponse{}, err
57+
}
58+
59+
return logInUserResponse, nil
2060
}
2161

2262
// GetAllUsers implements UserService.
@@ -105,10 +145,11 @@ func (u *UserServiceImpl) RegisterUser(createUserReq request.CreateUserRequest)
105145
return user, nil
106146
}
107147

108-
func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher) UserService {
148+
func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher, jwtUtils utils.JWTUtils) UserService {
109149
return &UserServiceImpl{
110150
UserRepository: userRepository,
111151
Validator: validator,
112152
PasswordHasher: passwordHaher,
153+
JWTUtils: jwtUtils,
113154
}
114155
}

api-users/utils/jwt_utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package utils
2+
3+
type JWTUtils interface {
4+
GenerateToken(userID string) (string, error)
5+
}

api-users/utils/jwt_utils_impl.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package utils
2+
3+
import (
4+
"os"
5+
"time"
6+
7+
"github.com/golang-jwt/jwt/v5"
8+
)
9+
10+
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
11+
12+
type JWTUtilsImpl struct{}
13+
14+
// GenerateToken implements JWTUtils.
15+
func (j *JWTUtilsImpl) GenerateToken(userID string) (string, error) {
16+
claims := jwt.MapClaims{
17+
"user_id": userID,
18+
"exp": time.Now().Add(time.Hour * 72).Unix(),
19+
}
20+
21+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
22+
23+
signedToken, err := token.SignedString(jwtSecret)
24+
if err != nil {
25+
return "", err
26+
}
27+
28+
return signedToken, nil
29+
}
30+
31+
func NewJWTUtils() JWTUtils {
32+
return &JWTUtilsImpl{}
33+
}

api-users/utils/password_hasher.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package utils
22

33
type PasswordHasher interface {
44
HashPassword(password string) (string, error)
5+
ComparePassword(hashedPassword, password string) error
56
}

api-users/utils/password_hasher_impl.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import (
77

88
type PasswordHasherImpl struct{}
99

10+
// ComparePassword implements PasswordHasher.
11+
func (p *PasswordHasherImpl) ComparePassword(hashedPassword string, password string) error {
12+
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
13+
}
14+
1015
// HashPassword implements PasswordHasher.
1116
func (p *PasswordHasherImpl) HashPassword(password string) (string, error) {
1217
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

authorizer/auth/jwt.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package auth
2+
3+
import "github.com/golang-jwt/jwt/v5"
4+
5+
type JWTValidator interface {
6+
ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error)
7+
}

authorizer/auth/jwt_impl.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package auth
2+
3+
import (
4+
"errors"
5+
6+
"github.com/golang-jwt/jwt/v5"
7+
)
8+
9+
type jwtValidator struct{}
10+
11+
// ValidateToken implements JWTValidator.
12+
func (v *jwtValidator) ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error) {
13+
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
14+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
15+
return nil, errors.New("unexpected signing method")
16+
}
17+
return secret, nil
18+
})
19+
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
if !token.Valid {
25+
return nil, errors.New("invalid token")
26+
}
27+
28+
claims, ok := token.Claims.(jwt.MapClaims)
29+
if !ok {
30+
return nil, errors.New("invalid token claims")
31+
}
32+
33+
return claims, nil
34+
}
35+
36+
func NewJWTValidator() JWTValidator {
37+
return &jwtValidator{}
38+
}

authorizer/aws/policy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package aws
2+
3+
import "github.com/aws/aws-lambda-go/events"
4+
5+
type Policy interface {
6+
GeneratePolicy(principalID, effect, resource string) events.APIGatewayCustomAuthorizerResponse
7+
}

0 commit comments

Comments
 (0)