DEV Community

loading...

My Journey into Go

michaelgv profile image Mike ・3 min read

I've always admired Go, it's a simplistic language, and I finally decided to give it a try. After numerous failures to understand how Go actually works, I decided to install the compiler, and write a simple program.

package main

import "fmt"

func main() {
    fmt.Println("Heck yes, it worked")
}
Enter fullscreen mode Exit fullscreen mode

Now just a little go build, and I've got my first application written in Go. It didn't seem hard or complex at all, but I struggled to understand the most how the multiple files would compile.

Take a PHP app for example, we can have N number of folders, files, structures, etc. and just do a simple require_once($file_path) from an autoloader and bob's your uncle. This was a simple approach, that I've always enjoyed. With go, we don't do that traditionally, we can go get, and import - but this isn't the same in my understanding as the PHP equivalent. I found if I just ran "go build" in a directory without putting the file name, everything I wanted compiled properly. That was awesome

Let's say you've got 5 go files:

  • webserver.go
  • api_route_pages.go
  • asset_pages.go
  • main.go
  • helpers.go

You use all these files, and they interact with methods together. If you try to run normally, go build main.go you're going to hit some errors - because you're strictly trying to compile one file; now if you run go build it won't error out.... why you may ask? Because you've included all files in your build.

Getting a little SQL with it

Now that I figured out how to get off the ground, I wanted to now bring in some SQL, with authentication logic, and here's how I did it.

Introducing the "Auth" service

I made a new file called "auth_service.go" - this would serve to have all my logical functions inside of it for authentication, including register, login, and validate session token.

We're going to need a few packages to do this:

import "golang.org/x/crypto/pbkdf2"

import (
    "crypto/rand"
    "crypto/sha512"
    "encoding/hex"
    "fmt"
    "strings"
    "crypto/subtle"
    "log"
)
Enter fullscreen mode Exit fullscreen mode

Creating Secure Password

We want to have our passwords encrypted, so we're using a salt, and doing pbkdf2, and finally hex encoding to string SALT:PASSWORD, and returning it.

func authMakePassword(password string) string {
    salt := make([]byte, 16)
    _, err := rand.Read(salt)
    checkErr(err)
    passwordHash := pbkdf2.Key([]byte(password), salt, 8192, 64, sha512.New)
    return hex.EncodeToString(salt) + ":" + hex.EncodeToString(passwordHash)
}
Enter fullscreen mode Exit fullscreen mode

Validating our password

In order to validate our password, we split the strings, run DecodeString for the hex parts, and we'll compare our actual password hash verses the provided password hash. If this is successful, we return true, otherwise we'll return false.

func authCheckPassword(password string, actualPasswordCombined string) bool {
    passwordParts := strings.Split(actualPasswordCombined, ":")
    salt, _ := hex.DecodeString(passwordParts[0])
    actualPasswordHash, _ := hex.DecodeString(passwordParts[1])
    providedPasswordHash := pbkdf2.Key([]byte(password), salt, 8192, 64, sha512.New)
    return subtle.ConstantTimeCompare(actualPasswordHash, providedPasswordHash) == 1
}
Enter fullscreen mode Exit fullscreen mode

It worked.

Putting this together, we can safely make our own authLogin function, which will simulate this:

func authLogin(username string, password string) (int, error) {
    db := MakeDatabase()
    if len(password) > 255 {
        return 0, fmt.Errorf("password is too long, max length is %s", "255")
    }

    rows := db.Query("SELECT id, password FROM users WHERE username = ?", username)
    if !rows.Next() {
        log.Printf("Authentication failure on user=%s: bad username (%s)", username, username)
        return 0, fmt.Errorf("invalid_username=%s", username)
    }
    var userId int
    var actualPasswordCombined string
    rows.Scan(&userId, &actualPasswordCombined)
    rows.Close()

    if authCheckPassword(password, actualPasswordCombined) {
        log.Printf("Authentication successful for user=%s", username)
        return userId, nil
    } else {
        log.Printf("Authentication failure on user=%s: bad username (%s)", username, username)
        return 0, fmt.Errorf("invalid_username=%s", username)
    }
}
Enter fullscreen mode Exit fullscreen mode

We first make our database connection, check the length, if it's too long we reject it, if it's not we permit it. We then get the id, password from the users table, validate it, and then we return success or failure.

I'm still working on it, though

It's definitely a work in progress, you can follow my progress here

Discussion (3)

Collapse
matteojoliveau profile image
Matteo Joliveau

It is more idiomatic in Go to have the package name act as the domain/namespace indicator.
I noticed your functions all begin with "auth". It would be correct to put auth_service.go in a package called auth, so your functions actually become auth.CheckPassword(), auth.Login() etc. (Notice the capital letter on the function names. In Go, a function with a capital is public, otherwise it is package-private).

Collapse
inkeliz profile image
Inkeliz • Edited

I think you can use an more secure password derivation, the Argon2, which was the winner of PHC

The Golang has the Argon2i and Argon2id "natively" under "golang.org/x/crypto/argon2", godoc.org/golang.org/x/crypto/argon2. If you need to use the Argon2d, for some reason, you can modify the API too.

Collapse
iridakos profile image
Forem Open with the Forem app