Skip to content

Commit

Permalink
[add]: add functionality for change and forgot password
Browse files Browse the repository at this point in the history
  • Loading branch information
goodylili committed Oct 11, 2023
1 parent c3a0424 commit fb12df5
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 30 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ The service is designed with security in mind, implementing measures to prevent

## Features
- [x] User Management
- [ ] User Authentication
- [ ] User Registration: Allows new users to create an account.
- [ ] User Login: Allows existing users to log in.
- [ ] Session Management
- [ ] Create Session: Creates a session when a user logs in.
- [ ] Destroy Session: Destroys the session when a user logs out.
- [ ] Password Management
- [ ] Password Reset: Allows users to reset their password.
- [ ] Password Hashing: Hashes passwords before storing them in the database.
- [x] User Authentication
- [x] User Registration: Allows new users to create an account.
- [x] User Login: Allows existing users to log in.
- [x] Session Management
- [x] Create Session: Creates a session when a user logs in.
- [x] Destroy Session: Destroys the session when a user logs out.
- [x] Password Management
- [x] Password Reset: Allows users to reset their password.
- [x] Password Hashing: Hashes passwords before storing them in the database.
- [ ] Social Authentication: Allows users to log in using their social media accounts.
- [ ] GitHub Authentication: Allows users to log in through GitHub
- [ ] Google Authentication: Allows users to log in through Google
- [ ] Personal Authentication: Implementing Personal OAuth in Go for the app
- [ ] JWT Authentication: Uses JSON Web Tokens (JWT) for secure information transmission.
- [x] JWT Authentication: Uses JSON Web Tokens (JWT) for secure information transmission.
- [ ] Security Measures
- [ ] SQL Injection Prevention: Prevents SQL injection attacks.
- [ ] CSRF Prevention: Prevents Cross-Site Request Forgery (CSRF) attacks.
Expand Down
8 changes: 5 additions & 3 deletions pkg/api/handler/handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handler

import (
"Reusable-Auth-System/pkg/auth/jwt"
"Reusable-Auth-System/pkg/users"
"context"
"fmt"
Expand Down Expand Up @@ -61,12 +62,13 @@ func (h *Handler) mapRoutes() {
v1.POST("/", h.CreateUser)
v1.GET("/:id", h.GetUserByID)
v1.GET("/email/:email", h.GetByEmail)
v1.GET("/username/:username", h.GetByUsername)
v1.GET("/username/:username", jwt.AuthMiddleWare(), h.GetByUsername)
v1.GET("/:username", h.GetByUsername)
v1.GET("/full_name/:full_name", h.GetUserByFullName)
v1.PUT("/:id", h.UpdateUserByID)
v1.GET("/full_name/:full_name", jwt.AuthMiddleWare(), h.GetUserByFullName)
v1.PUT("/:id", jwt.AuthMiddleWare(), h.UpdateUserByID)
v1.PUT("/:id", h.SetActivity)
v1.POST("/sign_in", h.SignIn)
v1.POST("/sign_out", jwt.AuthMiddleWare(), h.SignOut)
}

}
Expand Down
43 changes: 43 additions & 0 deletions pkg/api/handler/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,46 @@ func (h *Handler) Ping(c *gin.Context) {

c.JSON(http.StatusOK, gin.H{"message": "pong"})
}

func (h *Handler) SignOut(c *gin.Context) {
// Delete the access_token cookie
c.SetCookie("access_token", "", -1, "/", "", false, true)

c.JSON(http.StatusOK, gin.H{"message": "User signed out successfully"})
}

// ChangePassword is the handler for the change password route
func (h *Handler) ChangePassword(c *gin.Context) {
var req struct {
Username string `json:"username"`
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.Users.ChangePassword(c, req.Username, req.OldPassword, req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
}

// ForgotPassword is the handler for the forgot password route
func (h *Handler) ForgotPassword(c *gin.Context) {
var req struct {
Email string `json:"email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.Users.ForgotPassword(c, req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Password reset link sent successfully"})
}
44 changes: 28 additions & 16 deletions pkg/auth/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,69 @@ import (
"github.com/golang-jwt/jwt"
"net/http"
"os"
"strings"
"time"
)

func GenerateAccessJWT(username string) (string, error) {
secret := os.Getenv("JWT_SECRET")
token := jwt.New(jwt.SigningMethodEdDSA)
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(24 * time.Hour)
claims["exp"] = time.Now().Add(4 * time.Hour).Unix()
claims["authorized"] = true
claims["user"] = username
tokenString, err := token.SignedString(secret)
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return tokenString, nil
}

// AuthMiddleWare is a function that returns a Gin middleware handler.
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
const BearerSchema = "Bearer "
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
return
}
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, BearerSchema))
if tokenString == "" {
// Retrieve the token string from the "access_token" cookie
tokenString, err := c.Cookie("access_token")
if err != nil {
// If there's an error (e.g., the cookie is not present), abort the request and respond with an error
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token required"})
return
}

// jwt.Parse is used to parse the JWT token string obtained from the cookie.
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if jwt.GetSigningMethod("EdDSA") != token.Method {
return nil, jwt.ErrSignatureInvalid
// Check if the signing method used in the token matches the expected signing method (HS256 in this case).
if jwt.GetSigningMethod("HS256") != token.Method {
return nil, jwt.ErrSignatureInvalid // Return an error if the signing method does not match.
}
// Return the key used for signing the token. This key is obtained from an environment variable.
return []byte(os.Getenv("JWT_SECRET")), nil
})

if err != nil || !token.Valid {
// If there's an error in parsing the token or if the token is invalid, abort the request and respond with an error.
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}

// Assert that the claims in the token are of type jwt.MapClaims, which is a map of strings to interfaces.
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
// If the assertion fails or the token is invalid, abort the request and respond with an error.
if !ok || !token.Valid {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse claims"})
return
}

// Obtain the expiration time claim from the token and assert it to a float64 (since it's a Unix timestamp).
expiry, ok := claims["exp"].(float64)
// If the assertion fails or the expiration time is in the past, abort the request and respond with an error.
if !ok || time.Unix(int64(expiry), 0).Before(time.Now()) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token expired"})
return
}

// Set the username claim from the token in the Gin context for use in subsequent handlers.
c.Set("username", claims["user"])
// Call c.Next() to pass the request to the next handler in the chain.
c.Next()
}
}
1 change: 0 additions & 1 deletion pkg/auth/login/login.go

This file was deleted.

38 changes: 38 additions & 0 deletions pkg/database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ func (d *Database) ResetPassword(ctx context.Context, newUser users.User) error

return nil
}

func (d *Database) UpdateUserRoleID(ctx context.Context, id uint, newRoleID int64) error {
var existingUser User
if err := d.Client.WithContext(ctx).Where("id = ?", id).First(&existingUser).Error; err != nil {
Expand Down Expand Up @@ -316,3 +317,40 @@ func (d *Database) SignIn(ctx context.Context, username, password string) error
// If the passwords match, sign-in is successful, return nil (no error)
return nil
}

// ChangePassword changes the user's password.
func (d *Database) ChangePassword(ctx context.Context, username, oldPassword, newPassword string) error {
var user User
err := d.Client.WithContext(ctx).Where("username = ?", username).First(&user).Error
if err != nil {
return fmt.Errorf("error fetching user by username: %w", err)
}

if !ComparePassword(oldPassword, user.Password) {
return errors.New("invalid old password")
}

hashedNewPassword, err := HashPassword(newPassword)
if err != nil {
return fmt.Errorf("error hashing new password: %w", err)
}

result := d.Client.WithContext(ctx).Model(&User{}).
Where("username = ?", username).
Updates(map[string]interface{}{"password": hashedNewPassword})

if result.Error != nil {
return fmt.Errorf("error updating password: %w", result.Error)
}

return nil
}

// ForgotPassword handles the forgot password process.
func (d *Database) ForgotPassword(ctx context.Context, email string) error {
_, err := d.GetByEmail(ctx, email)
if err != nil {
return fmt.Errorf("error fetching user by email: %w", err)
}
return nil
}
25 changes: 25 additions & 0 deletions pkg/users/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Service interface {
DeleteUserByID(context.Context, int64) error
Ping(ctx context.Context) error
SignIn(context.Context, string, string) error
ChangePassword(ctx context.Context, username string, oldPassword, newPassword string) error
ForgotPassword(ctx context.Context, email string) error
}

// StoreImpl is the blueprint for the users logic
Expand Down Expand Up @@ -179,3 +181,26 @@ func (u *StoreImpl) SignIn(ctx context.Context, username, password string) error
}
return nil
}

// ChangePassword changes the password
func (u *StoreImpl) ChangePassword(ctx context.Context, username, oldPassword, newPassword string) error {
if err := u.Store.ChangePassword(ctx, username, oldPassword, newPassword); err != nil {
log.WithFields(logrus.Fields{
"error": err,
}).Error("Error changing password")
return err

}
return nil
}

// ForgotPassword - sends a password reset link to the user's email
func (u *StoreImpl) ForgotPassword(ctx context.Context, email string) error {
if err := u.Store.ForgotPassword(ctx, email); err != nil {
log.WithFields(logrus.Fields{
"error": err,
}).Error("Error sending password reset link")
return err
}
return nil
}

0 comments on commit fb12df5

Please sign in to comment.