@@ -0,0 +1,3 @@
|
||||
// Package auth provides password hashing and secure token primitives used by
|
||||
// Gardomatic authentication flows.
|
||||
package auth
|
||||
@@ -0,0 +1,71 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Password holds a bcrypt hash and, while constructing a new password, its
|
||||
// plaintext value for policy validation. Plaintext is never exposed.
|
||||
type Password struct {
|
||||
plaintext *string
|
||||
hash []byte
|
||||
}
|
||||
|
||||
// NewPassword reconstructs a password value from an existing bcrypt hash.
|
||||
func NewPassword(hash []byte) *Password {
|
||||
return &Password{hash: hash}
|
||||
}
|
||||
|
||||
// Set hashes plaintextPassword and replaces the stored hash.
|
||||
func (p *Password) Set(plaintextPassword string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.plaintext = &plaintextPassword
|
||||
p.hash = hash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a defensive copy of the bcrypt hash.
|
||||
func (p *Password) Get() []byte {
|
||||
return p.hash
|
||||
}
|
||||
|
||||
// Matches reports whether plaintextPassword matches the stored bcrypt hash.
|
||||
func (p *Password) Matches(plaintextPassword string) (bool, error) {
|
||||
err := bcrypt.CompareHashAndPassword(p.hash, []byte(plaintextPassword))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Validate adds password-hash validation errors to v.
|
||||
func (p *Password) Validate(v *validate.Validator) {
|
||||
if p.plaintext != nil {
|
||||
ValidatePasswordPlaintext(v, *p.plaintext)
|
||||
}
|
||||
|
||||
if p.hash == nil {
|
||||
panic("missing password hash for user")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePasswordPlaintext applies the password policy to plaintext input.
|
||||
func ValidatePasswordPlaintext(v *validate.Validator, plaintext string) {
|
||||
v.Check(plaintext != "", "password", "must be provided")
|
||||
v.Check(len(plaintext) >= 8, "password", "must be at least 8 bytes long")
|
||||
v.Check(len(plaintext) <= 72, "password", "must not be more than 72 bytes long")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"time"
|
||||
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopeActivation identifies account activation tokens.
|
||||
ScopeActivation = "activation"
|
||||
// ScopeAuthentication identifies bearer authentication tokens.
|
||||
ScopeAuthentication = "authentication"
|
||||
// ScopePasswordReset identifies password reset tokens.
|
||||
ScopePasswordReset = "password-reset"
|
||||
)
|
||||
|
||||
// Token carries a one-time plaintext token and the hash persisted by storage.
|
||||
type Token struct {
|
||||
Plaintext string `json:"token"`
|
||||
Hash []byte `json:"-"`
|
||||
UserID int `json:"-"`
|
||||
Expiry time.Time `json:"expiry"`
|
||||
Scope string `json:"-"`
|
||||
}
|
||||
|
||||
// NewToken creates a cryptographically random token for a user and scope.
|
||||
func NewToken(userID int, ttl time.Duration, scope string) Token {
|
||||
token := Token{
|
||||
Plaintext: rand.Text(),
|
||||
UserID: userID,
|
||||
Expiry: time.Now().Add(ttl),
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(token.Plaintext))
|
||||
token.Hash = hash[:]
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// Validate adds token consistency errors to v.
|
||||
func (tk Token) Validate(v *validate.Validator) {
|
||||
ValidateTokenPlaintext(v, tk.Plaintext)
|
||||
}
|
||||
|
||||
// ValidateTokenPlaintext checks the expected format of a user-supplied token.
|
||||
func ValidateTokenPlaintext(v *validate.Validator, tokenPlaintext string) {
|
||||
v.Check(tokenPlaintext != "", "token", "must be provided")
|
||||
v.Check(len(tokenPlaintext) == 26, "token", "must be 26 bytes long")
|
||||
}
|
||||
Reference in New Issue
Block a user