Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Authentication system using Golang and Sveltekit - User registration

IntroductionWith the basic setup laid bare, it's time to build a truly useful API service for our authentication system. In this article, we will delve into user registration, storage in the database, Password hashing using argon2id, sending templated emails, and generating truly random and secure tokens, among others. Let's get on! Source codeThe source code for this series is hosted on GitHub via: Sirneij / go-auth A fullstack session-based authentication system using golang and sveltekit go-auth View on GitHub Implementation Step 1: Create the user's database schemaWe need a database table to store our application's users' data. To generate and migrate a schema, we'll use golang migrate. Kindly follow these instructions to install it on your Operating system. To create a pair of migration files (up and down) for our user table, issue the following command in your terminal and at the root of your project:~/Documents/Projects/web/go-auth/go-auth-backend$ migrate create -seq -ext=.sql -dir=./migrations create_users_table-seq instructs the CLI to use sequential numbering as against the default, which is the Unix timestamp. We opted to use .sql file extensions for the generated files by passing -ext. The generated files will live in the migrations folder we created in the previous article and -dir allows us to specify that. Lastly, we fed it with the real name of the files we want to create. You should see two files in the migrations folder by name. Kindly open the up and fill in the following schema:-- migrations/000001_create_users_table.up.sql-- Add up migration script here-- User tableCREATE TABLE IF NOT EXISTS users( id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, is_active BOOLEAN DEFAULT FALSE, is_staff BOOLEAN DEFAULT FALSE, is_superuser BOOLEAN DEFAULT FALSE, thumbnail TEXT NULL, date_joined TIMESTAMPTZ NOT NULL DEFAULT NOW());CREATE INDEX IF NOT EXISTS users_id_email_is_active_indx ON users (id, email, is_active);-- Create a domain for phone data typeCREATE DOMAIN phone AS TEXT CHECK( octet_length(VALUE) BETWEEN 1 /*+*/ + 8 AND 1 /*+*/ + 15 + 3 AND VALUE ~ '^\+\d+$');-- User details table (One-to-one relationship)CREATE TABLE user_profile ( id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL UNIQUE, phone_number phone NULL, birth_date DATE NULL, github_link TEXT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);CREATE INDEX IF NOT EXISTS users_detail_id_user_id ON user_profile (id, user_id);In the down file, we should have:-- migrations/000001_create_users_table.down.sql-- Add down migration script hereDROP TABLE IF EXISTS users;DROP TABLE IF EXISTS user_profile;We have been using these schemas right from when we started the authentication series.Next, we need to execute the files so that those tables will be really created in our database:migrate -path=./migrations -database= upEnsure you replace with your real database URL. If everything goes well, your table should now be created in your database.It should be noted that instead of manually migrating the database, we could do that automatically, at start-up, in the main() function. Step 2: Setting up our user modelTo abstract away interacting with the database, we will create some sort of model, an equivalent of Django's model. But before then, let's create a type for our users in internal/data/user_types.go (create the file as it doesn't exist yet):// internal/data/user_types.gopackage dataimport ( "database/sql" "errors" "time" "github.com/google/uuid" "goauthbackend.johnowolabiidogun.dev/internal/types")type UserProfile struct { ID *uuid.UUID `json:"id"` UserID *uuid.UUID `json:"user_id"` PhoneNumber *string `json:"phone_number"` BirthDate types.NullTime `json:"birth_date"` GithubLink *string `json:"github_link"`}type User struct { ID uuid.UUID `json:"id"` Email string `json:"email"` Password password `json:"-"` FirstName string `json:"first_name"` LastName string `json:"last_name"` IsActive bool `json:"is_active"` IsStaff bool `json:"is_staff"` IsSuperuser bool `json:"is_superuser"` Thumbnail *string `json:"thumbnail"` DateJoined time.Time `json:"date_joined"` Profile UserProfile `json:"profile"`}type password struct { plaintext *string hash string}type UserModel struct { DB *sql.DB}type UserID struct { Id uuid.UUID}var ( ErrDuplicateEmail = errors.New("duplicate email"))These are just the basic types we'll be working on within this system. You will notice that there are three columns: names of the fields, field types, and the "renames" of the fields in JSON. The last column is very useful because, in Go, field names MUST start with capital letters for them to be accessible outside their package. The same goes to type names. Therefore, we need a way to properly send field names to requesting users and Go helps with that using the built-in encoding/json package. Notice also that our Password field was renamed to -. This omits that field entirely from the JSON responses it generates. How cool is that! We also defined a custom password type. This makes it easier to generate the hash of our users' passwords.Then, there is this not-so-familiar types.NullTime in the UserProfile type. It was defined in internal/types/time.go:// internal/types/time.gopackage typesimport ( "fmt" "reflect" "strings" "time" "github.com/lib/pq")// NullTime is an alias for pq.NullTime data typetype NullTime pq.NullTime// Scan implements the Scanner interface for NullTimefunc (nt *NullTime) Scan(value interface{}) error { var t pq.NullTime if err := t.Scan(value); err != nil { return err } // if nil then make Valid false if reflect.TypeOf(value) == nil { *nt = NullTime{t.Time, false} } else { *nt = NullTime{t.Time, true} } return nil}// MarshalJSON for NullTimefunc (nt *NullTime) MarshalJSON() ([]byte, error) { if !nt.Valid { return []byte("null"), nil } val := fmt.Sprintf("\"%s\"", nt.Time.Format(time.RFC3339)) return []byte(val), nil}const dateFormat = "2006-01-02"// UnmarshalJSON for NullTimefunc (nt *NullTime) UnmarshalJSON(b []byte) error { t, err := time.Parse(dateFormat, strings.Replace( string(b), "\"", "", -1, )) if err != nil { return err } nt.Time = t nt.Valid = true return nil}The reason for this is the difficulty encountered while working with possible null values for users' birthdates. This article explains it quite well and the code above was some modification of the code there.It should be noted that to use UUID in Go, you need an external package (we used github.com/google/uuid in our case, so install it with go get github.com/google/uuid).Next is handling password hashing:// internal/data/user_password.gopackage dataimport ( "log" "github.com/alexedwards/argon2id")func (p *password) Set(plaintextPassword string) error { hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams) if err != nil { return err } p.plaintext = &plaintextPassword p.hash = hash return nil}func (p *password) Matches(plaintextPassword string) (bool, error) { match, err := argon2id.ComparePasswordAndHash(plaintextPassword, p.hash) if err != nil { log.Fatal(err) } return match, nil}We used github.com/alexedwards/argon2id package to assist in hashing and matching our users' passwords. It's Go's implementation of argon2id. The Set "method" does the hashing when a user registers whereas Matches confirms it when such a user wants to log in.To validate users' inputs, a very go thing to do, we have:// internal/data/user_validation.gopackage dataimport "goauthbackend.johnowolabiidogun.dev/internal/validator"func ValidateEmail(v *validator.Validator, email string) { v.Check(email != "", "email", "email must be provided") v.Check(validator.Matches(email, validator.EmailRX), "email", "email must be a valid email address")}func ValidatePasswordPlaintext(v *validator.Validator, password string) { v.Check(password != "", "password", "password must be provided") v.Check(len(password) >= 8, "password", "password must be at least 8 bytes long") v.Check(len(password)



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Authentication system using Golang and Sveltekit - User registration

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×