[Go] Try HTTP Authentication 2
Masui Masanori
Posted on June 26, 2023
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
}
}
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]))
}
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 ""
}
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
}
Posted on June 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024