HN-like selfhosted forums

Code git clone ssh://git.poy.ooo:6969/~rii/district ZIP
auth/auth.go / 3KiB #b99a6119
Raw
package authimport (	"context"	"crypto/rand"	"crypto/sha256"	"database/sql"	"encoding/base64"	"errors"	"time"	"github.com/alexedwards/argon2id"	"github.com/google/uuid"	"github.com/jackc/pgx/v5/pgconn"	"github.com/nxrmqlly/district/store")const (	SessionLifetime = 30 * 24 * time.Hour)const (	argonIterations  = 3	argonMemory      = 64 * 1024 // 64 MiB	argonParallelism = 4	argonKeyLen      = 32	argonSaltLen     = 16)type Service struct {	queries *store.Queries}func New(queries *store.Queries) *Service {	return &Service{queries: queries}}func randomToken() ([]byte, error) {	token := make([]byte, 32)	if _, err := rand.Read(token); err != nil {		return nil, err	}	return token, nil}func sha256sum(t []byte) []byte {	sha := sha256.Sum256(t)	return sha[:]}func HashPassword(password string) (string, error) {	return argon2id.CreateHash(password, &argon2id.Params{		Iterations:  argonIterations,		Memory:      argonMemory,		Parallelism: argonParallelism,		SaltLength:  argonSaltLen,		KeyLength:   argonKeyLen,	})}func VerifyPassword(password, hash string) (bool, error) {	return argon2id.ComparePasswordAndHash(password, hash)}type AuthSession struct {	ID        uuid.UUID	UserID    uuid.UUID	Username  string	CreatedAt time.Time	ExpiresAt time.Time	LastUsed  *time.Time}type AuthUser struct {	ID       uuid.UUID	Username string	Email    string}func (s *Service) CreateSession(ctx context.Context, userID uuid.UUID) (string, error) {	sessionToken, err := randomToken()	if err != nil {		return "", err	}	tokHash := sha256sum(sessionToken)	_, err = s.queries.NewSession(ctx, store.NewSessionParams{		UserID:    userID,		TokenHash: tokHash,		ExpiresAt: time.Now().Add(SessionLifetime),	})	if err != nil {		return "", err	}	return base64.RawURLEncoding.EncodeToString(sessionToken), nil}func (s *Service) GetSession(ctx context.Context, tokenB64 string) (*AuthSession, error) {	raw, err := base64.RawURLEncoding.DecodeString(tokenB64)	if err != nil {		return nil, err	}	hashed := sha256sum([]byte(raw))	se, err := s.queries.GetSessionByTokenHash(ctx, hashed)	if err != nil {		return nil, err	}	return &AuthSession{		ID:        se.ID,		UserID:    se.UserID,		Username:  se.SessionUsername,		CreatedAt: se.CreatedAt,		ExpiresAt: se.ExpiresAt,		LastUsed:  se.LastUsed,	}, nil}func (s *Service) RegisterUser(ctx context.Context, email, username, passwd string) (*AuthUser, error) {	passwdHash, err := HashPassword(passwd)	if err != nil {		return nil, err	}	user, err := s.queries.NewUser(ctx, store.NewUserParams{		Username:   username,		Email:      email,		PasswdHash: passwdHash,	})	if err != nil {		var pgErr *pgconn.PgError		if errors.As(err, &pgErr) && pgErr.Code == "23505" {			switch pgErr.ConstraintName {			case "users_username_key", "users_username_lower_idx":				return nil, ErrUsernameTaken			case "users_email_key":				return nil, ErrEmailInUse			}		}		return nil, err	}	return &AuthUser{		ID:       user.ID,		Username: user.Username,		Email:    user.Email,	}, nil}func (s *Service) Authenticate(ctx context.Context, login, password string) (*AuthUser, error) {	user, err := s.queries.GetUserByLogin(ctx, login)	if err != nil {		if errors.Is(err, sql.ErrNoRows) {			return nil, ErrInvalidCredentials		}		return nil, err	}	ok, err := VerifyPassword(password, user.PasswdHash)	if err != nil {		return nil, err	}	if !ok {		return nil, ErrInvalidCredentials	}	// * creds matched	return &AuthUser{		ID:       user.ID,		Username: user.Username,		Email:    user.Email,	}, nil}func (s *Service) RevokeSession(ctx context.Context, sessionID uuid.UUID) error {	return s.queries.RevokeSession(ctx, sessionID)}