(FEAT): Removed the need for sessions, going to use JWT's in cookies.
This works very well, just need to determine what routes will be protected and such. For now, a simple system is setup, with more to come. For now, this is a WIP and needs some light work. But auth is almost complete.
This commit is contained in:
parent
d3d9c8a9e2
commit
8e4a0deec8
@ -262,13 +262,6 @@ found in **OTHER** section.
|
||||
- [x] GoogleRefreshToken () text
|
||||
- [x] Created (Required) date/time stamp
|
||||
|
||||
- [ ] Sessions: Represents a single user-session.
|
||||
- [ ] ID (PK) Serial
|
||||
- [ ] UserId (FK: User.Id, Required) Serial
|
||||
- [ ] Token (Required) text
|
||||
- [ ] Expiration (Required) date/time stamp
|
||||
- [ ] Created (Required) date/time stamp
|
||||
|
||||
- [ ] Engagements: Represents a single engagement from a single user.
|
||||
- [ ] ID (PK) Serial
|
||||
- [ ] Message () text (Used to store any relevant notes, if needed)
|
||||
|
||||
1
go.mod
1
go.mod
@ -22,6 +22,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
|
||||
@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
@ -22,9 +23,20 @@ func GoogleCallback(ctx *gin.Context) {
|
||||
code string = ctx.Query("code")
|
||||
)
|
||||
|
||||
if dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||
// TODO: Do something real, not just return data
|
||||
if jwt, dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, gin.H{"googleUserInfo": googleUserInfo, "dbUser": dbUser})
|
||||
// TODO: Update these values when using a real domain. Maybe an ENV?
|
||||
ctx.SetCookie(
|
||||
"jwt_token",
|
||||
jwt,
|
||||
int(time.Now().Add(7*24*time.Hour).Sub(time.Now()).Seconds()),
|
||||
"/",
|
||||
"localhost",
|
||||
false, // TODO: True in prod
|
||||
true,
|
||||
)
|
||||
ctx.JSON(http.StatusOK, gin.H{"jwt": jwt, "googleUserInfo": googleUserInfo, "dbUser": dbUser})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
)
|
||||
|
||||
@ -13,3 +19,69 @@ func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.Handler
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func JwtAuthMiddleWare(jwtSecretKey []byte) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
// NOTE: Option One: From auth header
|
||||
//
|
||||
// authHeader := ctx.GetHeader("Authorization")
|
||||
//
|
||||
// if authHeader == "" {
|
||||
// ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization required."})
|
||||
// ctx.Abort()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// parts := strings.SplitN(authHeader, " ", 2)
|
||||
// if !(len(parts) == 2 && parts[0] == "Bearer") {
|
||||
// ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header must be Bearer token"})
|
||||
// ctx.Abort()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// tokenString := parts[1]
|
||||
|
||||
// TODO: How are we handling faliure?
|
||||
|
||||
tokenString, err := ctx.Cookie("jwt_token")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "JWT token cookie not found"})
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims := &domain.JwtClaims{}
|
||||
// token, err := jwt.ParseWithClaims(tokenString, claims, )
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrSignatureInvalid) {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token signature"})
|
||||
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Token expired or not yet valid"})
|
||||
} else {
|
||||
log.Printf("JWT parsing error: %v", err)
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
}
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Found: Set the values
|
||||
ctx.Set("userId", claims.UserId)
|
||||
ctx.Set("userEmail", claims.Email)
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,17 +101,26 @@ func (s *Server) Start() {
|
||||
}
|
||||
|
||||
func (s *Server) Setup() *Server {
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
panic("Could not load env file")
|
||||
}
|
||||
|
||||
jwtSecret := []byte(os.Getenv("JWT_SECRET"))
|
||||
|
||||
// Initialize and inject dependencies
|
||||
userRepo := repository.NewUserRepository(s.DB)
|
||||
userService := service.NewUserService(userRepo)
|
||||
authService := service.NewAuthService(userRepo)
|
||||
authService := service.NewAuthService(userRepo, jwtSecret)
|
||||
|
||||
deps := &domain.InjectedDependencies{
|
||||
UserService: userService,
|
||||
AuthService: authService,
|
||||
}
|
||||
|
||||
// Apply middleware
|
||||
s.Router.Use(DepedencyInjectionMiddleware(deps))
|
||||
s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
||||
|
||||
// Wrap all routes with a version
|
||||
router_v1 := s.Router.Group("/v1")
|
||||
@ -125,6 +134,12 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
// API router endpoints
|
||||
router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })
|
||||
router_api.GET("/tmp", func(ctx *gin.Context) {
|
||||
userId := ctx.MustGet("userId").(int)
|
||||
userEmail := ctx.MustGet("userEmail").(string)
|
||||
|
||||
ctx.JSON(200, gin.H{"id": userId, "email": userEmail})
|
||||
})
|
||||
|
||||
// WEB router endpoints
|
||||
router_web.GET("/login", handlers.LoginPage)
|
||||
|
||||
@ -3,8 +3,11 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||
"golang.org/x/oauth2"
|
||||
@ -27,6 +30,7 @@ import (
|
||||
// AuthService implements the domain.AuthService defined in the domain module.
|
||||
type AuthService struct {
|
||||
userRepository domain.UserRepository
|
||||
jwtSecret []byte
|
||||
}
|
||||
|
||||
// Compile-time check to ensure the AuthService implements domain.AuthService
|
||||
@ -34,8 +38,11 @@ var _ domainAuth.AuthService = (*AuthService)(nil)
|
||||
|
||||
// NewAuthService creates a user service object which can be passed into the context. The service
|
||||
// requires a user repository which it will use to hit the database when needed.
|
||||
func NewAuthService(userRepository domain.UserRepository) domainAuth.AuthService {
|
||||
return &AuthService{userRepository: userRepository}
|
||||
func NewAuthService(userRepository domain.UserRepository, jwtSecret []byte) domainAuth.AuthService {
|
||||
return &AuthService{
|
||||
userRepository: userRepository,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
// GetGoogleAuthUrl generates a URL which is used to redirect the user to the Google sign in page.
|
||||
@ -53,42 +60,65 @@ func (s *AuthService) GetGoogleAuthUrl() string {
|
||||
|
||||
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
|
||||
// data. The data is then used to log the user in or create an account.
|
||||
func (s *AuthService) GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error) {
|
||||
func (s *AuthService) GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error) {
|
||||
// Ensure the state matches, prevents M.I.T.M. attacks
|
||||
if state != "randomstate" {
|
||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
||||
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
||||
}
|
||||
|
||||
// Get access token from Google
|
||||
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
||||
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
||||
}
|
||||
|
||||
// Use the access token to get user data
|
||||
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.GoogleUserInfo{}, err
|
||||
return "", domain.User{}, domain.GoogleUserInfo{}, err
|
||||
}
|
||||
|
||||
// Attempt to get the user, user is nil when they don't exit
|
||||
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
|
||||
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
|
||||
}
|
||||
|
||||
// A user was found
|
||||
if user != nil {
|
||||
return *user, googleUserInfo, nil
|
||||
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret)
|
||||
return jwt, *user, googleUserInfo, err
|
||||
}
|
||||
|
||||
// user did not exist, need to create one
|
||||
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
|
||||
return "", domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
|
||||
}
|
||||
|
||||
// temporary
|
||||
return newUser, googleUserInfo, nil
|
||||
jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret)
|
||||
return jwt, newUser, googleUserInfo, err
|
||||
|
||||
}
|
||||
|
||||
func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
|
||||
expiration := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
claims := &domainServer.JwtClaims{
|
||||
UserId: userId,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiration),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Subject: fmt.Sprintf("%d", userId),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to sign jwt: %s", err.Error())
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
@ -6,5 +6,5 @@ import (
|
||||
|
||||
type AuthService interface {
|
||||
GetGoogleAuthUrl() string
|
||||
GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error)
|
||||
GoogleAuthSuccess(state, code string) (string, domain.User, domain.GoogleUserInfo, error)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
)
|
||||
@ -9,3 +10,9 @@ type InjectedDependencies struct {
|
||||
UserService domainUser.UserService
|
||||
AuthService domainAuth.AuthService
|
||||
}
|
||||
|
||||
type JwtClaims struct {
|
||||
UserId int `json:"id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user