├── .gitignore ├── util ├── util.go └── errors.go ├── models └── user.go ├── security ├── password.go ├── token_test.go └── token.go ├── go.mod ├── routes ├── routes.go └── auth.go ├── main.go ├── db └── db.go ├── readme.md ├── repository └── users.go ├── go.sum └── controllers └── auth.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | type JError struct { 6 | Error string `json:"error"` 7 | } 8 | 9 | func NewJError(err error) JError { 10 | jerr := JError{"generic error"} 11 | if err != nil { 12 | jerr.Error = err.Error() 13 | } 14 | return jerr 15 | } 16 | 17 | func NormalizeEmail(email string) string { 18 | return strings.TrimSpace(strings.ToLower(email)) 19 | } 20 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/mgo.v2/bson" 7 | ) 8 | 9 | type User struct { 10 | Id bson.ObjectId `json:"id" bson:"_id"` 11 | Email string `json:"email" bson:"email"` 12 | Password string `json:"password" bson:"password"` 13 | CreatedAt time.Time `json:"created_at" bson:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` 15 | } 16 | -------------------------------------------------------------------------------- /util/errors.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidEmail = errors.New("invalid email") 7 | ErrEmailAlreadyExists = errors.New("email already exists") 8 | ErrEmptyPassword = errors.New("password can't be empty") 9 | ErrInvalidAuthToken = errors.New("invalid auth-token") 10 | ErrInvalidCredentials = errors.New("invalid credentials") 11 | ErrUnauthorized = errors.New("Unauthorized") 12 | ) 13 | -------------------------------------------------------------------------------- /security/password.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func EncryptPassword(password string) (string, error) { 6 | hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 7 | if err != nil { 8 | return "", err 9 | } 10 | return string(hashed), nil 11 | } 12 | 13 | func VerifyPassword(hashed, password string) error { 14 | return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(password)) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-fiber-auth-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible 7 | github.com/gofiber/fiber/v2 v2.5.0 8 | github.com/gofiber/jwt/v2 v2.1.0 9 | github.com/joho/godotenv v1.3.0 // indirect 10 | github.com/stretchr/testify v1.7.0 11 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 12 | gopkg.in/asaskevich/govalidator.v9 v9.0.0-20180315120708-ccb8e960c48f 13 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 14 | ) 15 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "go-fiber-auth-api/security" 5 | "go-fiber-auth-api/util" 6 | "net/http" 7 | 8 | "github.com/gofiber/fiber/v2" 9 | jwtware "github.com/gofiber/jwt/v2" 10 | ) 11 | 12 | type Routes interface { 13 | Install(app *fiber.App) 14 | } 15 | 16 | func AuthRequired(ctx *fiber.Ctx) error { 17 | return jwtware.New(jwtware.Config{ 18 | SigningKey: security.JwtSecretKey, 19 | SigningMethod: security.JwtSigningMethod, 20 | TokenLookup: "header:Authorization", 21 | ErrorHandler: func(c *fiber.Ctx, err error) error { 22 | return c. 23 | Status(http.StatusUnauthorized). 24 | JSON(util.NewJError(err)) 25 | }, 26 | })(ctx) 27 | } 28 | -------------------------------------------------------------------------------- /routes/auth.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "go-fiber-auth-api/controllers" 5 | 6 | "github.com/gofiber/fiber/v2" 7 | ) 8 | 9 | type authRoutes struct { 10 | authController controllers.AuthController 11 | } 12 | 13 | func NewAuthRoutes(authController controllers.AuthController) Routes { 14 | return &authRoutes{authController} 15 | } 16 | 17 | func (r *authRoutes) Install(app *fiber.App) { 18 | app.Post("/signup", r.authController.SignUp) 19 | app.Post("/signin", r.authController.SignIn) 20 | app.Get("/users", AuthRequired, r.authController.GetUsers) 21 | app.Get("/users/:id", AuthRequired, r.authController.GetUser) 22 | app.Put("/users/:id", AuthRequired, r.authController.PutUser) 23 | app.Delete("/users/:id", AuthRequired, r.authController.DeleteUser) 24 | } 25 | -------------------------------------------------------------------------------- /security/token_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | func TestNewToken(t *testing.T) { 12 | id := bson.NewObjectId() 13 | token, err := NewToken(id.Hex()) 14 | assert.NoError(t, err) 15 | assert.NotEmpty(t, token) 16 | } 17 | 18 | func TestParseToken(t *testing.T) { 19 | id := bson.NewObjectId() 20 | token, err := NewToken(id.Hex()) 21 | assert.NoError(t, err) 22 | assert.NotEmpty(t, token) 23 | 24 | payload, err := ParseToken(token) 25 | assert.NoError(t, err) 26 | assert.Equal(t, id.Hex(), payload.Id) 27 | assert.Equal(t, id.Hex(), payload.Issuer) 28 | assert.Equal(t, time.Now().Year(), time.Unix(payload.IssuedAt, 0).Year()) 29 | assert.Equal(t, time.Now().Month(), time.Unix(payload.IssuedAt, 0).Month()) 30 | assert.Equal(t, time.Now().Day(), time.Unix(payload.IssuedAt, 0).Day()) 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go-fiber-auth-api/controllers" 5 | "go-fiber-auth-api/db" 6 | "go-fiber-auth-api/repository" 7 | "go-fiber-auth-api/routes" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/gofiber/fiber/v2" 12 | "github.com/gofiber/fiber/v2/middleware/cors" 13 | "github.com/gofiber/fiber/v2/middleware/logger" 14 | "github.com/joho/godotenv" 15 | ) 16 | 17 | func init() { 18 | err := godotenv.Load() 19 | if err != nil { 20 | log.Panicln(err) 21 | } 22 | } 23 | 24 | func main() { 25 | conn := db.NewConnection() 26 | defer conn.Close() 27 | 28 | app := fiber.New() 29 | app.Use(cors.New()) 30 | app.Use(logger.New()) 31 | app.Get("/", func(ctx *fiber.Ctx) error { 32 | return ctx.Status(http.StatusOK).JSON(fiber.Map{"message": "Hello World"}) 33 | }) 34 | 35 | usersRepo := repository.NewUsersRepository(conn) 36 | authController := controllers.NewAuthController(usersRepo) 37 | authRoutes := routes.NewAuthRoutes(authController) 38 | authRoutes.Install(app) 39 | 40 | log.Fatal(app.Listen(":8080")) 41 | } 42 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "gopkg.in/mgo.v2" 10 | ) 11 | 12 | type Connection interface { 13 | Close() 14 | DB() *mgo.Database 15 | } 16 | 17 | type conn struct { 18 | session *mgo.Session 19 | } 20 | 21 | func NewConnection() Connection { 22 | var c conn 23 | var err error 24 | url := getURL() 25 | c.session, err = mgo.Dial(url) 26 | if err != nil { 27 | log.Panicln(err.Error()) 28 | } 29 | return &c 30 | } 31 | 32 | func (c *conn) Close() { 33 | c.session.Close() 34 | } 35 | 36 | func (c *conn) DB() *mgo.Database { 37 | return c.session.DB(os.Getenv("DATABASE_NAME")) 38 | } 39 | 40 | func getURL() string { 41 | port, err := strconv.Atoi(os.Getenv("DATABASE_PORT")) 42 | if err != nil { 43 | log.Println("error on load db port from env:", err.Error()) 44 | port = 27017 45 | } 46 | return fmt.Sprintf("mongodb://%s:%s@%s:%d/%s", 47 | os.Getenv("DATABASE_USER"), 48 | os.Getenv("DATABASE_PASS"), 49 | os.Getenv("DATABASE_HOST"), 50 | port, 51 | os.Getenv("DATABASE_NAME")) 52 | } 53 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Golang Authentication API with Fiber MongoDB and JWT 2 | 3 | ## Run Database on Docker 4 | 5 | ```bash 6 | docker run -it --rm --name mongodb_container -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=admin -v mongodata:/data/db -d -p 27017:27017 mongo 7 | 8 | docker exec -it mongodb_container /bin/bash 9 | 10 | mongo -u admin -p admin --authenticationDatabase admin 11 | 12 | use mydb 13 | 14 | db.createUser({user: 'user', pwd: 'password', roles:[{'role': 'readWrite', 'db': 'mydb'}]}); 15 | 16 | # testing authentication with new user 17 | mongo -u user -p password --authenticationDatabase mydb 18 | 19 | use mydb 20 | 21 | show collections 22 | ``` 23 | 24 | ## Create Your Env File 25 | 26 | ```bash 27 | DATABASE_USER=user 28 | DATABASE_PASS=password 29 | DATABASE_HOST=127.0.0.1 30 | DATABASE_PORT=27017 31 | DATABASE_NAME=mydb 32 | JWT_SECRET_KEY=secret 33 | ``` 34 | 35 | ## Run API 36 | 37 | ```bash 38 | go run main.go 39 | ``` 40 | 41 | ### References: 42 | 43 | https://github.com/gofiber/fiber 44 | 45 | https://sodocumentation.net/go/topic/10161/jwt-authorization-in-go -------------------------------------------------------------------------------- /security/token.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "fmt" 5 | "go-fiber-auth-api/util" 6 | "os" 7 | "time" 8 | 9 | jwt "github.com/form3tech-oss/jwt-go" 10 | ) 11 | 12 | var ( 13 | JwtSecretKey = []byte(os.Getenv("JWT_SECRET_KEY")) 14 | JwtSigningMethod = jwt.SigningMethodHS256.Name 15 | ) 16 | 17 | func NewToken(userId string) (string, error) { 18 | claims := jwt.StandardClaims{ 19 | Id: userId, 20 | Issuer: userId, 21 | IssuedAt: time.Now().Unix(), 22 | ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), 23 | } 24 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 25 | return token.SignedString(JwtSecretKey) 26 | } 27 | 28 | func validateSignedMethod(token *jwt.Token) (interface{}, error) { 29 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 30 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 31 | } 32 | return JwtSecretKey, nil 33 | } 34 | 35 | func ParseToken(tokenString string) (*jwt.StandardClaims, error) { 36 | claims := new(jwt.StandardClaims) 37 | token, err := jwt.ParseWithClaims(tokenString, claims, validateSignedMethod) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var ok bool 42 | claims, ok = token.Claims.(*jwt.StandardClaims) 43 | if !ok || !token.Valid { 44 | return nil, util.ErrInvalidAuthToken 45 | } 46 | return claims, nil 47 | } 48 | -------------------------------------------------------------------------------- /repository/users.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "go-fiber-auth-api/db" 5 | "go-fiber-auth-api/models" 6 | 7 | "gopkg.in/mgo.v2" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | const UsersCollection = "users" 12 | 13 | type UsersRepository interface { 14 | Save(user *models.User) error 15 | Update(user *models.User) error 16 | GetById(id string) (user *models.User, err error) 17 | GetByEmail(email string) (user *models.User, err error) 18 | GetAll() (users []*models.User, err error) 19 | Delete(id string) error 20 | } 21 | 22 | type usersRepository struct { 23 | c *mgo.Collection 24 | } 25 | 26 | func NewUsersRepository(conn db.Connection) UsersRepository { 27 | return &usersRepository{conn.DB().C(UsersCollection)} 28 | } 29 | 30 | func (r *usersRepository) Save(user *models.User) error { 31 | return r.c.Insert(user) 32 | } 33 | 34 | func (r *usersRepository) Update(user *models.User) error { 35 | return r.c.UpdateId(user.Id, user) 36 | } 37 | 38 | func (r *usersRepository) GetById(id string) (user *models.User, err error) { 39 | err = r.c.FindId(bson.ObjectIdHex(id)).One(&user) 40 | return user, err 41 | } 42 | 43 | func (r *usersRepository) GetByEmail(email string) (user *models.User, err error) { 44 | err = r.c.Find(bson.M{"email": email}).One(&user) 45 | return user, err 46 | } 47 | 48 | func (r *usersRepository) GetAll() (users []*models.User, err error) { 49 | err = r.c.Find(bson.M{}).All(&users) 50 | return users, err 51 | } 52 | 53 | func (r *usersRepository) Delete(id string) error { 54 | return r.c.RemoveId(bson.ObjectIdHex(id)) 55 | } 56 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= 2 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/form3tech-oss/jwt-go v1.0.2 h1:Uc1WOWLA1SIUmdX+J9WdopklFeufogYuP67+tGPhH+4= 6 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= 7 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 8 | github.com/gofiber/fiber v1.14.6 h1:QRUPvPmr8ijQuGo1MgupHBn8E+wW0IKqiOvIZPtV70o= 9 | github.com/gofiber/fiber/v2 v2.1.0/go.mod h1:aG+lMkwy3LyVit4CnmYUbUdgjpc3UYOltvlJZ78rgQ0= 10 | github.com/gofiber/fiber/v2 v2.5.0 h1:yml405Um7b98EeMjx63OjSFTATLmX985HPWFfNUPV0w= 11 | github.com/gofiber/fiber/v2 v2.5.0/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0= 12 | github.com/gofiber/jwt v0.2.0 h1:D8uf+84DLFcnlaZsX89sXQ14hHbH4tyT0fN5hmaulEc= 13 | github.com/gofiber/jwt/v2 v2.1.0 h1:eTahBQ8vO73fcXjpQjv/xiKZqMjvDtw5QmsvdgWQg6w= 14 | github.com/gofiber/jwt/v2 v2.1.0/go.mod h1:sf3l8cbwW93qL0kM9jcX9QTc8VIJgXCO1RWquBJUqTg= 15 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 16 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 17 | github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= 18 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= 27 | github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM= 28 | github.com/valyala/fasthttp v1.18.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= 29 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= 30 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 33 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 36 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20201210223839-7e3030f88018 h1:XKi8B/gRBuTZN1vU9gFsLMm6zVz5FSCDzm8JYACnjy8= 44 | golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 46 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | gopkg.in/asaskevich/govalidator.v9 v9.0.0-20180315120708-ccb8e960c48f h1:RVvpqSdNKxt6sENjmw0kdyyv8r18TdpmYTrvUUg2qkc= 49 | gopkg.in/asaskevich/govalidator.v9 v9.0.0-20180315120708-ccb8e960c48f/go.mod h1:+MTrBL6wlsxv1uFXT6b9LWG7PJdrvUJEjl8tXOlk9OU= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 52 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 54 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /controllers/auth.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "go-fiber-auth-api/models" 6 | "go-fiber-auth-api/repository" 7 | "go-fiber-auth-api/security" 8 | "go-fiber-auth-api/util" 9 | "log" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/form3tech-oss/jwt-go" 15 | "github.com/gofiber/fiber/v2" 16 | "gopkg.in/asaskevich/govalidator.v9" 17 | "gopkg.in/mgo.v2" 18 | "gopkg.in/mgo.v2/bson" 19 | ) 20 | 21 | type AuthController interface { 22 | SignUp(ctx *fiber.Ctx) error 23 | SignIn(ctx *fiber.Ctx) error 24 | GetUser(ctx *fiber.Ctx) error 25 | GetUsers(ctx *fiber.Ctx) error 26 | PutUser(ctx *fiber.Ctx) error 27 | DeleteUser(ctx *fiber.Ctx) error 28 | } 29 | 30 | type authController struct { 31 | usersRepo repository.UsersRepository 32 | } 33 | 34 | func NewAuthController(usersRepo repository.UsersRepository) AuthController { 35 | return &authController{usersRepo} 36 | } 37 | 38 | func (c *authController) SignUp(ctx *fiber.Ctx) error { 39 | var newUser models.User 40 | err := ctx.BodyParser(&newUser) 41 | if err != nil { 42 | return ctx. 43 | Status(http.StatusUnprocessableEntity). 44 | JSON(util.NewJError(err)) 45 | } 46 | newUser.Email = util.NormalizeEmail(newUser.Email) 47 | if !govalidator.IsEmail(newUser.Email) { 48 | return ctx. 49 | Status(http.StatusBadRequest). 50 | JSON(util.NewJError(util.ErrInvalidEmail)) 51 | } 52 | exists, err := c.usersRepo.GetByEmail(newUser.Email) 53 | if err == mgo.ErrNotFound { 54 | if strings.TrimSpace(newUser.Password) == "" { 55 | return ctx. 56 | Status(http.StatusBadRequest). 57 | JSON(util.NewJError(util.ErrEmptyPassword)) 58 | } 59 | newUser.Password, err = security.EncryptPassword(newUser.Password) 60 | if err != nil { 61 | return ctx. 62 | Status(http.StatusBadRequest). 63 | JSON(util.NewJError(err)) 64 | } 65 | newUser.CreatedAt = time.Now() 66 | newUser.UpdatedAt = newUser.CreatedAt 67 | newUser.Id = bson.NewObjectId() 68 | err = c.usersRepo.Save(&newUser) 69 | if err != nil { 70 | return ctx. 71 | Status(http.StatusBadRequest). 72 | JSON(util.NewJError(err)) 73 | } 74 | return ctx. 75 | Status(http.StatusCreated). 76 | JSON(newUser) 77 | } 78 | 79 | if exists != nil { 80 | err = util.ErrEmailAlreadyExists 81 | } 82 | 83 | return ctx. 84 | Status(http.StatusBadRequest). 85 | JSON(util.NewJError(err)) 86 | } 87 | 88 | func (c *authController) SignIn(ctx *fiber.Ctx) error { 89 | var input models.User 90 | err := ctx.BodyParser(&input) 91 | if err != nil { 92 | return ctx. 93 | Status(http.StatusUnprocessableEntity). 94 | JSON(util.NewJError(err)) 95 | } 96 | input.Email = util.NormalizeEmail(input.Email) 97 | user, err := c.usersRepo.GetByEmail(input.Email) 98 | if err != nil { 99 | log.Printf("%s signin failed: %v\n", input.Email, err.Error()) 100 | return ctx. 101 | Status(http.StatusUnauthorized). 102 | JSON(util.NewJError(util.ErrInvalidCredentials)) 103 | } 104 | err = security.VerifyPassword(user.Password, input.Password) 105 | if err != nil { 106 | log.Printf("%s signin failed: %v\n", input.Email, err.Error()) 107 | return ctx. 108 | Status(http.StatusUnauthorized). 109 | JSON(util.NewJError(util.ErrInvalidCredentials)) 110 | } 111 | token, err := security.NewToken(user.Id.Hex()) 112 | if err != nil { 113 | log.Printf("%s signin failed: %v\n", input.Email, err.Error()) 114 | return ctx. 115 | Status(http.StatusUnauthorized). 116 | JSON(util.NewJError(err)) 117 | } 118 | return ctx. 119 | Status(http.StatusOK). 120 | JSON(fiber.Map{ 121 | "user": user, 122 | "token": fmt.Sprintf("Bearer %s", token), 123 | }) 124 | } 125 | 126 | func (c *authController) GetUser(ctx *fiber.Ctx) error { 127 | payload, err := AuthRequestWithId(ctx) 128 | if err != nil { 129 | return ctx. 130 | Status(http.StatusUnauthorized). 131 | JSON(util.NewJError(err)) 132 | } 133 | user, err := c.usersRepo.GetById(payload.Id) 134 | if err != nil { 135 | return ctx. 136 | Status(http.StatusInternalServerError). 137 | JSON(util.NewJError(err)) 138 | } 139 | return ctx. 140 | Status(http.StatusOK). 141 | JSON(user) 142 | } 143 | 144 | func (c *authController) GetUsers(ctx *fiber.Ctx) error { 145 | users, err := c.usersRepo.GetAll() 146 | if err != nil { 147 | return ctx. 148 | Status(http.StatusInternalServerError). 149 | JSON(util.NewJError(err)) 150 | } 151 | return ctx. 152 | Status(http.StatusOK). 153 | JSON(users) 154 | } 155 | 156 | func (c *authController) PutUser(ctx *fiber.Ctx) error { 157 | payload, err := AuthRequestWithId(ctx) 158 | if err != nil { 159 | return ctx. 160 | Status(http.StatusUnauthorized). 161 | JSON(util.NewJError(err)) 162 | } 163 | var update models.User 164 | err = ctx.BodyParser(&update) 165 | if err != nil { 166 | return ctx. 167 | Status(http.StatusUnprocessableEntity). 168 | JSON(util.NewJError(err)) 169 | } 170 | update.Email = util.NormalizeEmail(update.Email) 171 | if !govalidator.IsEmail(update.Email) { 172 | return ctx. 173 | Status(http.StatusBadRequest). 174 | JSON(util.NewJError(util.ErrInvalidEmail)) 175 | } 176 | exists, err := c.usersRepo.GetByEmail(update.Email) 177 | if err == mgo.ErrNotFound || exists.Id.Hex() == payload.Id { 178 | user, err := c.usersRepo.GetById(payload.Id) 179 | if err != nil { 180 | return ctx. 181 | Status(http.StatusBadRequest). 182 | JSON(util.NewJError(err)) 183 | } 184 | user.Email = update.Email 185 | user.UpdatedAt = time.Now() 186 | err = c.usersRepo.Update(user) 187 | if err != nil { 188 | return ctx. 189 | Status(http.StatusUnprocessableEntity). 190 | JSON(util.NewJError(err)) 191 | } 192 | return ctx. 193 | Status(http.StatusOK). 194 | JSON(user) 195 | } 196 | 197 | if exists != nil { 198 | err = util.ErrEmailAlreadyExists 199 | } 200 | 201 | return ctx. 202 | Status(http.StatusBadRequest). 203 | JSON(util.NewJError(err)) 204 | } 205 | 206 | func (c *authController) DeleteUser(ctx *fiber.Ctx) error { 207 | payload, err := AuthRequestWithId(ctx) 208 | if err != nil { 209 | return ctx. 210 | Status(http.StatusUnauthorized). 211 | JSON(util.NewJError(err)) 212 | } 213 | err = c.usersRepo.Delete(payload.Id) 214 | if err != nil { 215 | return ctx. 216 | Status(http.StatusInternalServerError). 217 | JSON(util.NewJError(err)) 218 | } 219 | ctx.Set("Entity", payload.Id) 220 | return ctx.SendStatus(http.StatusNoContent) 221 | } 222 | 223 | func AuthRequestWithId(ctx *fiber.Ctx) (*jwt.StandardClaims, error) { 224 | id := ctx.Params("id") 225 | if !bson.IsObjectIdHex(id) { 226 | return nil, util.ErrUnauthorized 227 | } 228 | token := ctx.Locals("user").(*jwt.Token) 229 | payload, err := security.ParseToken(token.Raw) 230 | if err != nil { 231 | return nil, err 232 | } 233 | if payload.Id != id || payload.Issuer != id { 234 | return nil, util.ErrUnauthorized 235 | } 236 | return payload, nil 237 | } 238 | --------------------------------------------------------------------------------