[Go] Try HTTP Authentication 2
Tags: go
Published: 2023-06-26 17:16:21

## Intro
This time, I will try verifying user input password, and remaining authenticated.

I will use th project what I created last time.

* [[Go] Accessing PostgreSQL with Bun 1](https://dev.to/masanori_msl/go-accessing-postgresql-with-bun-1-47m9)

## Samples
* [web-db-sample - GitHub](https://github.com/masanori840816/web-db-sample)

## Verifying password
### Update hashed password generator
To verify the password enterd by user, it must be hashed with the same salt value and iterate count as when it was registered in the database.
However, the previous password generation function didn't hold them, so I will add them with "PasswordHasher" of ASP.NET Core Identity as a reference.

#### passwordHasher.go
```go
package hash

import (
	"bytes"
	"crypto/rand"
	"crypto/sha512"
	"encoding/base64"

	"golang.org/x/crypto/pbkdf2"

	"math/big"
)

// itelateCount(4) + salt length(4)
const fixedPasswordLength = 8
// Generate hashed password
func GeneratePasswordHash(password string) (string, error) {
	salt, err := generateRandomSalt(128 / 8)
	if err != nil {
		return "", err
	}
	result := generateHash(password, salt, 100_000, 256/8)
	return base64.URLEncoding.EncodeToString(result), nil
}
func generateHash(original string, salt []byte, iterateCount int, keyLength int) []byte {
	// Get base 64 encoded Hasu value to save the password
	key := pbkdf2.Key([]byte(original), salt, iterateCount, keyLength, sha512.New)
	// Add the iterate count, salt, salt length.
	results := make([]byte, len(key)+fixedPasswordLength+len(salt))
	writeNetworkByteOrder(results, 0, uint(iterateCount))
	writeNetworkByteOrder(results, 4, uint(len(salt)))
	// Add salt
	blockCopy(salt, 0, results, fixedPasswordLength, len(salt))
	// Add hashed password
	blockCopy(key, 0, results, fixedPasswordLength+len(salt), len(key))
	return results
}

// Generate a salt value
func generateRandomSalt(length int) ([]byte, error) {
	results := make([]byte, length)
	for i := 0; i < length; i++ {
		salt, err := rand.Int(rand.Reader, big.NewInt(255))
		if err != nil {
			return nil, err
		}
		results[i] = byte(salt.Int64())
	}
	return results, nil
}
func writeNetworkByteOrder(buffer []byte, offset int, value uint) {
	buffer[offset+0] = byte(value >> 24)
	buffer[offset+1] = byte(value >> 16)
	buffer[offset+2] = byte(value >> 8)
	buffer[offset+3] = byte(value >> 0)
}
func blockCopy(src []byte, srcOffset int, dst []byte, dstOffset int, copyLength int) {
	index := dstOffset
	for i := srcOffset; i < copyLength+srcOffset; i++ {
		dst[index] = src[i]
		index += 1
	}
}
```

### Verifying password
To verify password, the iterate count and the salt value are taken out from the hashed password first.
After that, the password enterd by user will be hashed with the salt value and the iterate count and compare them.

#### passwordHasher.go
```go
...
func VerifyPassword(inputPassword string, hashedPassword string) (bool, error) {
	decodedPassword, err := base64.URLEncoding.DecodeString(hashedPassword)
	if err != nil {
		return false, err
	}
    // Read the iterate count and the salt value
	iterateCount := readNetworkByteOrder(decodedPassword, 0)
	saltLength := readNetworkByteOrder(decodedPassword, 4)
	salt := make([]byte, saltLength)
	blockCopy(decodedPassword, fixedPasswordLength, salt, 0, int(saltLength))
	// hash the user input password with the iterate count and the salt value 
    hashedInput := generateHash(inputPassword, salt, int(iterateCount),
		len(decodedPassword)-(int(saltLength)+fixedPasswordLength))
    // compare the passwords
	return bytes.Equal(hashedInput, decodedPassword), nil
}
...
func readNetworkByteOrder(buffer []byte, offset int) uint {
	return ((uint)(buffer[offset]) << 24) |
		((uint)(buffer[offset+1]) << 16) |
		((uint)(buffer[offset+2]) << 8) |
		((uint)(buffer[offset+3]))
}
```

## Remaining authenticated
In this time, I will set JWT(JWS) into cookies to remain authenticated.

To generate and verify JWT, I will use "jwt-go".

* [golang-jwt/jwt - GitHub](https://github.com/golang-jwt/jwt)

#### signinManager.go
```go
package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"

	db "github.com/web-db-sample/db"
	dto "github.com/web-db-sample/dto"
)

const secretKey = "kwTL6Nnm.4gbTPBCU_6kveHEZg"

func Signin(w http.ResponseWriter, r *http.Request, dbCtx *db.BookshelfContext) (bool, error) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		return false, err
	}
	signinValue := &dto.SigninValues{}
	err = json.Unmarshal(body, &signinValue)
	if err != nil {
		return false, err
	}
	ctx := context.Background()
	result, userID, err := dbCtx.Users.Signin(&ctx, *signinValue)
	if err != nil {
		return false, err
	}
	if !result {
		return false, nil
	}
	token, err := generateToken(userID)
	if err != nil {
		return false, err
	}
	expiration := time.Now()
	expiration = expiration.AddDate(0, 0, 1)
	cookie := http.Cookie{Name: "AuthSample", Value: token, Expires: expiration, HttpOnly: true}
	http.SetCookie(w, &cookie)
	return result, nil
}
func Signout(w http.ResponseWriter) {
	cookie := http.Cookie{Name: "AuthSample", Value: "", Expires: time.Unix(0, 0), HttpOnly: true}
	http.SetCookie(w, &cookie)
}
func VerifyToken(w http.ResponseWriter, r *http.Request) (bool, int64) {
	tokenString := getToken(r)
	if len(tokenString) <= 0 {
		return false, -1
	}
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Don't forget to validate the alg is what you expect:
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("UNEXPECTED SIGNING METHOD: %v", token.Header["alg"])
		}
		return []byte(secretKey), nil
	})
	if err != nil {
		return false, -1
	}
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		return true, int64(claims["userid"].(float64))
	}
	return false, -1
}
func generateToken(userID int64) (string, error) {
	claims := jwt.MapClaims{
		"userid": userID,
		"exp":    time.Now().Add(time.Hour * 24).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// Add sign
	return token.SignedString([]byte(secretKey))
}
func getToken(r *http.Request) string {
	for _, c := range r.Cookies() {
		if c.Name != "AuthSample" {
			continue
		}
		// remove "AuthSample=" from the cookie value
		result := c.String()
		// If the client has a valid cookie, it can retrieve the value.
		return strings.Replace(result, "AuthSample=", "", 1)
	}
	return ""
}
```

#### users.go
```go
...
func (u Users) Signin(ctx *context.Context, value dto.SigninValues) (bool, int64, error) {
	user := new(models.AppUsers)
	err := u.db.NewSelect().
		Model(user).
		Where("name=?", value.UserName).
		Limit(1).
		Scan(*ctx)
	if err != nil {
		// ignore no rows error
		if err != sql.ErrNoRows {
			return false, -1, err
		}
	}
	result, err := hash.VerifyPassword(value.Password, user.Password)
	if err != nil {
		return false, -1, err
	}
	if result {
		return result, user.ID, nil
	}
	return result, -1, nil
}
```