DEV Community

Masui Masanori
Masui Masanori

Posted on • Updated on

[Go] Try HTTP Authentication 2

#go

Intro

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

I will use th project what I created last time.

Samples

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

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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

...
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]))
}
Enter fullscreen mode Exit fullscreen mode

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".

signinManager.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 ""
}
Enter fullscreen mode Exit fullscreen mode

users.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
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)