104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
// Package auth provides password hashing, JWT, and token helpers used to
|
|
// authenticate and authorize API requests.
|
|
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alexedwards/argon2id"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// HashPassword hashes a plaintext password using argon2id.
|
|
func HashPassword(password string) (string, error) {
|
|
return argon2id.CreateHash(password, argon2id.DefaultParams)
|
|
}
|
|
|
|
// CheckPasswordHash reports whether password matches the given argon2id hash.
|
|
func CheckPasswordHash(password string, hash string) (bool, error) {
|
|
return argon2id.ComparePasswordAndHash(password, hash)
|
|
}
|
|
|
|
// MakeJWT creates a signed HS256 JWT for the given user that expires after
|
|
// expiresIn.
|
|
func MakeJWT(userID uuid.UUID, tokenSecret []byte, expiresIn time.Duration) (string, error) {
|
|
token := jwt.New(jwt.GetSigningMethod(jwt.SigningMethodHS256.Name))
|
|
|
|
token.Claims = &jwt.RegisteredClaims{
|
|
Issuer: "chirpy-access",
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
|
|
Subject: userID.String(),
|
|
}
|
|
|
|
return token.SignedString(tokenSecret)
|
|
}
|
|
|
|
// ValidateJWT verifies the token signature and returns the subject's user ID.
|
|
func ValidateJWT(tokenString string, tokenSecret []byte) (uuid.UUID, error) {
|
|
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) {
|
|
return tokenSecret, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
userId, err := token.Claims.GetSubject()
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
validUserId, err := uuid.Parse(userId)
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
return validUserId, nil
|
|
}
|
|
|
|
// GetBearerToken extracts the "Bearer" token from the Authorization header.
|
|
func GetBearerToken(headers http.Header) (string, error) {
|
|
authorizationHeader := headers.Get("Authorization")
|
|
|
|
if strings.TrimSpace(authorizationHeader) == "" {
|
|
return "", errors.New("Missing Authorization header")
|
|
}
|
|
|
|
token := strings.TrimPrefix(authorizationHeader, "Bearer ")
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// GetAPIKey extracts the API key from the Authorization header using the
|
|
// "ApiKey" scheme.
|
|
func GetAPIKey(headers http.Header) (string, error) {
|
|
authorizationHeader := headers.Get("Authorization")
|
|
|
|
if strings.TrimSpace(authorizationHeader) == "" {
|
|
return "", errors.New("Missing Authorization header")
|
|
}
|
|
|
|
apiKey := strings.TrimPrefix(authorizationHeader, "ApiKey ")
|
|
|
|
return apiKey, nil
|
|
}
|
|
|
|
// MakeRefreshToken generates a cryptographically random, hex-encoded refresh
|
|
// token.
|
|
func MakeRefreshToken() string {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(bytes)
|
|
}
|