Skip to content

Commit fc4c643

Browse files
committed
feat: Implement JWT authentication and token generation for OAuth2 login
1 parent 8061cf8 commit fc4c643

File tree

11 files changed

+104
-26
lines changed

11 files changed

+104
-26
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ An API designed to manage events, supporting features like event creation, user
8181
5. **Authentication** (Needs first deploy and a domain to simplify OAuth2 google configuration to test...)
8282
- [x] Configure in GCP.
8383
- [x] Implement OAuth2 login with Google using OpenID for authentication.
84-
- [ ] Return the JWT token if the authentication is successful.
84+
- [x] Return the JWT token if the authentication is successful.
85+
- [ ] Refactor: Change to Struct-Based Controller,Service and Repository.
86+
- [ ] Refresh token endpoint.
87+
- [ ] Private routes.
8588

8689
### **Phase 2: Intermediate Features**
8790
1. **Event Management**
88-
- [ ] Refactor: Change to Struct-Based Controller,Service and Repository
8991
- [ ] Update and delete events.
9092
- [ ] Add pagination for event listings.
9193
- [ ] Protect endpoints with Private endpoints.

config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type Config struct {
2323
GoogleClientID string
2424
GoogleClientSecret string
2525
GoogleRedirectURL string
26+
27+
// JWT
28+
JWTSecret string
2629
}
2730

2831
func LoadConfig() *Config {
@@ -49,6 +52,8 @@ func LoadConfig() *Config {
4952
GoogleClientID: getEnv("GOOGLE_CLIENT_ID", ""),
5053
GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""),
5154
GoogleRedirectURL: getEnv("GOOGLE_REDIRECT_URL", ""),
55+
56+
JWTSecret: getEnv("JWT_SECRET", ""),
5257
}
5358
}
5459

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
require (
5050
github.com/gin-contrib/cors v1.7.2
5151
github.com/gin-gonic/gin v1.10.0
52+
github.com/golang-jwt/jwt v3.2.2+incompatible
5253
github.com/golang-jwt/jwt/v4 v4.5.1
5354
github.com/jackc/pgpassfile v1.0.0 // indirect
5455
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL
4242
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
4343
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
4444
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
45+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
46+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
4547
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
4648
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
4749
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

internal/auth/auth_url/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
type Controller struct {
11-
Service Service
11+
Service *Service
1212
}
1313

1414
// @Summary Get the authentication URL for a provider

internal/auth/callback/controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
type Controller struct {
11-
Service Service
11+
Service *Service
1212
}
1313

1414
// @Summary Handle the callback from the authentication provider
@@ -24,10 +24,10 @@ type Controller struct {
2424
func (c *Controller) HandleCallback(ctx *gin.Context) {
2525
provider := ctx.Param("provider")
2626
code := ctx.Query("code")
27-
user, err := c.Service.ProcessCallback(ctx, provider, code)
27+
tokenResponse, err := c.Service.ProcessCallback(ctx, provider, code)
2828
if err != nil {
2929
response.Error(ctx, http.StatusBadRequest, "auth.callback.failed", err.Error())
3030
return
3131
}
32-
response.Success(ctx, http.StatusOK, "auth.callback.success", user, nil)
32+
response.Success(ctx, http.StatusOK, "auth.callback.success", tokenResponse, nil)
3333
}

internal/auth/callback/repository.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,15 @@ import (
1010
"github.com/jackc/pgx/v5/pgxpool"
1111
)
1212

13-
type Repository interface {
14-
FindOrCreateUser(data providers.UserInfo) (models.User, error)
15-
}
16-
17-
type repository struct {
13+
type Repository struct {
1814
db *pgxpool.Pool
1915
}
2016

21-
func NewRepository(db *pgxpool.Pool) Repository {
22-
return &repository{db}
17+
func NewRepository(db *pgxpool.Pool) *Repository {
18+
return &Repository{db}
2319
}
2420

25-
func (r *repository) FindOrCreateUser(data providers.UserInfo) (models.User, error) {
21+
func (r *Repository) FindOrCreateUser(data providers.UserInfo) (models.User, error) {
2622
var user models.User
2723
query := `SELECT id, email, name, profile_picture_url, role, created_at, updated_at FROM users WHERE email = $1`
2824
err := r.db.QueryRow(context.Background(), query, data.Email).Scan(&user.ID, &user.Email, &user.Name, &user.ProfilePictureURL, &user.Role, &user.CreatedAt, &user.UpdatedAt)

internal/auth/callback/service.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,52 @@ import (
44
"context"
55
"errors"
66

7+
"github.com/idylicaro/event-management/internal/auth/jwt"
78
"github.com/idylicaro/event-management/internal/auth/providers"
8-
"github.com/idylicaro/event-management/internal/models"
9+
dto "github.com/idylicaro/event-management/internal/dto/auth"
910
)
1011

1112
type Service struct {
1213
Providers map[string]providers.OAuthProvider
13-
Repository Repository
14+
Repository *Repository
15+
JWTService *jwt.Service
1416
}
1517

16-
func (s *Service) ProcessCallback(ctx context.Context, providerName, code string) (models.User, error) {
18+
func (s *Service) ProcessCallback(ctx context.Context, providerName, code string) (*dto.TokenResponse, error) {
1719
provider, exists := s.Providers[providerName]
1820
if !exists {
19-
return models.User{}, errors.New("provider not supported")
21+
return nil, errors.New("provider not supported")
2022
}
2123

2224
tokens, err := provider.ExchangeCode(ctx, code)
2325
if err != nil {
24-
return models.User{}, err
26+
return nil, err
2527
}
2628

2729
// Exchange the code for a token and fetch user data
2830
userData, err := provider.GetUserInfo(tokens.AccessToken)
2931
if err != nil {
30-
return models.User{}, err
32+
return nil, err
3133
}
3234

3335
// Create or find the user in the database
3436
user, err := s.Repository.FindOrCreateUser(userData)
3537
if err != nil {
36-
return models.User{}, err
38+
return nil, err
3739
}
3840

39-
return user, nil
41+
accessToken, err := s.JWTService.GenerateAccessToken(user)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
refreshToken, err := s.JWTService.GenerateRefreshToken(user)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return &dto.TokenResponse{
52+
AccessToken: accessToken,
53+
RefreshToken: refreshToken,
54+
}, nil
4055
}

internal/auth/jwt/service.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package jwt
2+
3+
import (
4+
"time"
5+
6+
"github.com/golang-jwt/jwt"
7+
"github.com/idylicaro/event-management/internal/models"
8+
)
9+
10+
// Constants for token durations
11+
const (
12+
AccessTokenDuration = time.Hour * 1
13+
RefreshTokenDuration = time.Hour * 24 * 7
14+
)
15+
16+
type Service struct {
17+
SecretKey []byte
18+
}
19+
20+
// NewService creates a new instance of the JWT Service
21+
func NewService(secretKey []byte) *Service {
22+
return &Service{SecretKey: secretKey}
23+
}
24+
25+
// GenerateAccessToken generates an access token for the user
26+
func (s *Service) GenerateAccessToken(user models.User) (string, error) {
27+
return s.generateToken(user, AccessTokenDuration)
28+
}
29+
30+
// GenerateRefreshToken generates a refresh token for the user
31+
func (s *Service) GenerateRefreshToken(user models.User) (string, error) {
32+
return s.generateToken(user, RefreshTokenDuration)
33+
}
34+
35+
// generateToken is a helper function to generate JWT tokens
36+
func (s *Service) generateToken(user models.User, duration time.Duration) (string, error) {
37+
claims := jwt.MapClaims{
38+
"user_id": user.ID,
39+
"exp": time.Now().Add(duration).Unix(),
40+
}
41+
42+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
43+
tokenString, err := token.SignedString(s.SecretKey)
44+
if err != nil {
45+
return "", err
46+
}
47+
48+
return tokenString, nil
49+
}

internal/auth/routes.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/idylicaro/event-management/config"
66
"github.com/idylicaro/event-management/internal/auth/auth_url"
77
"github.com/idylicaro/event-management/internal/auth/callback"
8+
"github.com/idylicaro/event-management/internal/auth/jwt"
89
"github.com/idylicaro/event-management/internal/auth/providers"
910
"github.com/jackc/pgx/v5/pgxpool"
1011
)
@@ -18,12 +19,13 @@ func RegisterAuthRoutes(router *gin.RouterGroup, db *pgxpool.Pool, cfg config.Co
1819
),
1920
}
2021

21-
authUrlService := auth_url.Service{Providers: providers}
22-
authUrlController := auth_url.Controller{Service: authUrlService}
22+
authUrlService := &auth_url.Service{Providers: providers}
23+
authUrlController := &auth_url.Controller{Service: authUrlService}
2324
router.GET("/:provider/url", authUrlController.GetAuthURL)
2425

26+
jwtService := jwt.NewService([]byte(cfg.JWTSecret))
2527
callbackRepo := callback.NewRepository(db)
26-
callbackService := callback.Service{Providers: providers, Repository: callbackRepo}
27-
callbackController := callback.Controller{Service: callbackService}
28+
callbackService := &callback.Service{Providers: providers, Repository: callbackRepo, JWTService: jwtService}
29+
callbackController := &callback.Controller{Service: callbackService}
2830
router.GET("/:provider/callback", callbackController.HandleCallback)
2931
}

0 commit comments

Comments
 (0)