I am going to use PostgreSQL for this project, so let's create a database. The superuser on my computer is cfeng. If you don't have a role or wish to create a separate role for this project, then just do the following
$ psql postgres
postgres=# create role <name> superuser login;
Create a database named go_academy_userauth with owner pointing to whichever role you like. I am using cfeng on my computer.
$ psql postgres
postgres=# create database go_academy_userauth with owner=cfeng;
Actually just in case you don't remember the password to your ROLE, do the following
postgres=# alter user <your_username> with password <whatever you like>
Example
Create cfeng
postgres=# create role cfeng superuser login;
Create database
postgres=# create database go_academy_userauth with owner=cfeng;
Update password
postgres=# alter user cfeng with password 'cfeng';
Project Dependencies
I am going to introduce couple new open source libraries for this project:
spf13/cobra
sirupsen/logrus
labstack/echo
jinzhu/gorm
Project User Authentication
This project is going to use JWT based authentication instead of the traditional session based authentication. For the detailed comparison of the two, take a look at this article.
This project is not going to use real JWT for simplicity sake, instead it uses a mock token. A real token can be generated using verified open source libraries, for more information go to JWT.io.
Nevertheless, it is important to show how JWT authentication works.
Authentication Endpoints
We expose endpoints on the server for users to sign in (authenticate) or register an account. The server is responsible for returning a token to the client side. Once user receives the token, he/she will embed this token into his/her request headers when he/she needs to access resources from the user. In practice, we would have two different services to complete the whole authentication cycle but we will combine the two services into one for this project.
Once front end receives the token, it should put it into local storage. Next time when user opens his/her browser, the client application should use the same token to verify that user is indeed signed in.
User Resource Endpoints
Since we are not using real JWT token, front end needs to make requests to server to ask for user information. We need to expose some user endpoints for that.
There are two primary resources on our server, i.e. users and messages.
// User is a user model.typeUserstruct {// Both ID uint`gorm:"column:id" json:"id"` Name string`gorm:"column:name" json:"name" ` Email string`gorm:"column:email" json:"email"` JWTToken string`gorm:"column:jwt_token" json:"jwt_token,omitempty"`// JSON only Password string`sql:"-" json:"password,omitempty"`// Database only CreatedAt time.Time`gorm:"column:created_at" json:"-"` UpdatedAt time.Time`gorm:"column:updated_at" json:"-"` PasswordDigest []byte`gorm:"column:password_digest" json:"-"`}
// Message is a model for messages.typeMessagestruct { ID uint`gorm:"column:id" json:"id"` CreatedAt time.Time`gorm:"column:created_at" json:"created_at"` UpdatedAt time.Time`gorm:"column:updated_at" json:"-"` Body string`gorm:"column:body" json:"body"`// Foreign keys SenderID uint`gorm:"column:sender_id" json:"sender_id"` Sender *User`gorm:"foreignkey:sender_id" json:"sender,omitempty"` ReceiverID uint`gorm:"column:receiver_id" json:"receiver_id"` Receiver *User`gorm:"foreignkey:receiver_id" json:"receiver,omitempty"`}
Migrations
Instead of using auto migration feature of GORM, I prefer to write our own SQL because it gives us greater flexibility and better organization.
CREATETABLEusers ( id SERIALPRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE,nameVARCHAR(255), email VARCHAR(255), jwt_token VARCHAR(255), password_digest BYTEA);CREATEUNIQUE INDEXON users(name);CREATEUNIQUE INDEXon users(email);CREATEUNIQUE INDEXON users(jwt_token);CREATETABLEmessages ( id SERIALPRIMARY KEY, created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE, sender_id INTEGERREFERENCES users(id), receiver_id INTEGERREFERENCES users(id), body TEXT);CREATEINDEXON messages(sender_id);CREATEINDEXON messages(receiver_id);
Put everything together
Let's start off with creating a skeleton for the project. Details will be discussed in the video section. Create a project file structure as follows.
Create two commands inside cmd package, one for migration and one for running server.
run_server.go
packagecmdimport ("io""net/http""os""github.com/labstack/echo/v4""github.com/labstack/echo/v4/middleware""github.com/jinzhu/gorm""github.com/spf13/cobra"// Driver for Postgres _ "github.com/jinzhu/gorm/dialects/postgres")// RunServerCmd is a command to run server from terminal.var RunServerCmd =&cobra.Command{ Use: "runserver", Short: "run user authentication server", RunE: runServer,}funcrunServer(cmd *cobra.Command, args []string) error { _, err := gorm.Open("postgres", pgAddr)if err !=nil {return err } srv := echo.New() srv.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "HTTP[${time_rfc3339}] ${method} ${path} status=${status} latency=${latency_human}\n", Output: io.MultiWriter(os.Stdout), })) srv.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, })) srv.File("/", "public/index.html") srv.Static("/assets", "public/assets")if err := srv.Start(":8080"); err !=nil {return err }returnnil}
packagecmdimport ("fmt""github.com/golang-migrate/migrate""github.com/sirupsen/logrus""github.com/spf13/cobra" _ "github.com/lib/pq"// Driver _ "github.com/golang-migrate/migrate/database/postgres"// Driver _ "github.com/golang-migrate/migrate/source/file"// Driver)const ( host ="localhost" port ="5432" user ="cfeng" password ="cfeng" database ="go_academy_userauth" ssl ="sslmode=disable" migrationDir ="file://./migrations/")var log = logrus.WithFields(logrus.Fields{"pkg": "cmd",})var pgAddr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?%s", user, password, host, port, database, ssl)// RunMigrationsCmd is a command to run migration.var RunMigrationsCmd =&cobra.Command{ Use: "runmigrations", Short: "run migration on database", RunE: runMigrations,}funcrunMigrations(cmd *cobra.Command, args []string) error { migration, err := migrate.New(migrationDir, pgAddr)if err !=nil {return err } log.Info("performing reset on database")if err = migration.Drop(); err !=nil {return err }if err := migration.Up(); err !=nil {return err } log.Info("migration has been performed successfully")returnnil}
Video
Now your project should be able to compile and you should be able to run migration on the database.
go install && userauth runmigrations
In the video, I will discuss how to write each endpoint.
Additional Resource
If you want to learn more about session storage, security, encryption, and many other topics relating to web applications, take a look at this GitBook.