2021-10-08 12:39:31 +00:00
|
|
|
// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
|
|
|
|
//
|
2021-02-09 11:53:33 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
2021-02-08 16:47:42 +00:00
|
|
|
package sqlite
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-05-11 09:05:17 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
2021-02-08 16:47:42 +00:00
|
|
|
"database/sql"
|
2021-05-11 09:05:17 +00:00
|
|
|
"encoding/base64"
|
2021-02-08 16:47:42 +00:00
|
|
|
"fmt"
|
|
|
|
|
2021-04-01 06:05:07 +00:00
|
|
|
"github.com/friendsofgo/errors"
|
2021-05-11 09:05:17 +00:00
|
|
|
"github.com/mattn/go-sqlite3"
|
2021-02-08 16:47:42 +00:00
|
|
|
"github.com/volatiletech/sqlboiler/v4/boil"
|
|
|
|
"github.com/volatiletech/sqlboiler/v4/queries/qm"
|
2021-04-05 07:12:05 +00:00
|
|
|
"go.mindeco.de/http/auth"
|
2021-03-18 16:49:52 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2021-02-08 16:47:42 +00:00
|
|
|
|
2022-11-07 09:18:13 +00:00
|
|
|
"github.com/ssbc/go-ssb-room/v2/roomdb"
|
|
|
|
"github.com/ssbc/go-ssb-room/v2/roomdb/sqlite/models"
|
|
|
|
weberrors "github.com/ssbc/go-ssb-room/v2/web/errors"
|
2021-02-08 16:47:42 +00:00
|
|
|
)
|
|
|
|
|
2021-03-02 16:14:02 +00:00
|
|
|
// compiler assertion to ensure the struct fullfills the interface
|
2021-03-10 15:44:46 +00:00
|
|
|
var _ roomdb.AuthFallbackService = (*AuthFallback)(nil)
|
2021-02-08 16:47:42 +00:00
|
|
|
|
|
|
|
type AuthFallback struct {
|
|
|
|
db *sql.DB
|
|
|
|
}
|
|
|
|
|
2021-04-05 07:12:05 +00:00
|
|
|
var redirectPasswordAuthErr = weberrors.ErrRedirect{
|
|
|
|
Path: "/fallback/login",
|
|
|
|
Reason: auth.ErrBadLogin,
|
|
|
|
}
|
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
// Check receives the loging and password (in clear) and checks them accordingly.
|
|
|
|
// Login might be a registered alias or a ssb id who belongs to a member.
|
2021-03-09 12:39:43 +00:00
|
|
|
// If it's a valid combination it returns the user ID, or an error if they are not.
|
2021-03-18 16:49:52 +00:00
|
|
|
func (af AuthFallback) Check(login, password string) (interface{}, error) {
|
2021-02-08 16:47:42 +00:00
|
|
|
ctx := context.Background()
|
2021-05-11 09:05:17 +00:00
|
|
|
|
|
|
|
var memberID = int64(-1)
|
|
|
|
|
|
|
|
// try looking up an alias first
|
|
|
|
loginIsAlias, err := models.Aliases(qm.Where("name = ?", login)).One(ctx, af.db)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
// something else went wrong
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// did not find an alias, try as ssb reference
|
|
|
|
member, err := models.Members(qm.Where("pub_key = ?", login)).One(ctx, af.db)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return nil, redirectPasswordAuthErr
|
|
|
|
}
|
|
|
|
|
|
|
|
// member found by ssb id, use their id
|
|
|
|
memberID = member.ID
|
|
|
|
} else {
|
|
|
|
// found an alias, use the corresponding member ID
|
|
|
|
memberID = loginIsAlias.MemberID
|
|
|
|
}
|
|
|
|
|
|
|
|
foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", memberID)).One(ctx, af.db)
|
2021-02-08 16:47:42 +00:00
|
|
|
if err != nil {
|
2021-04-01 06:05:07 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2021-04-05 07:12:05 +00:00
|
|
|
return nil, redirectPasswordAuthErr
|
2021-04-01 06:05:07 +00:00
|
|
|
}
|
2021-02-08 16:47:42 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
err = bcrypt.CompareHashAndPassword(foundPassword.PasswordHash, []byte(password))
|
2021-02-08 16:47:42 +00:00
|
|
|
if err != nil {
|
2021-04-05 07:12:05 +00:00
|
|
|
return nil, redirectPasswordAuthErr
|
2021-02-08 16:47:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
return foundPassword.MemberID, nil
|
2021-02-08 16:47:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-12 12:41:47 +00:00
|
|
|
func (af AuthFallback) SetPassword(ctx context.Context, memberID int64, password string) error {
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
2021-05-11 09:05:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to hash password for member")
|
|
|
|
}
|
|
|
|
|
|
|
|
// this is a silly upsert construction which sqlboiler-sqlite doesnt support nativly
|
|
|
|
return transact(af.db, func(tx *sql.Tx) error {
|
|
|
|
|
|
|
|
foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", memberID)).One(ctx, tx)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
// something else went wrong
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// not found => insert new entry
|
|
|
|
var newPasswordEntry models.FallbackPassword
|
|
|
|
newPasswordEntry.PasswordHash = hashed
|
|
|
|
newPasswordEntry.MemberID = memberID
|
|
|
|
err = newPasswordEntry.Insert(ctx, tx, boil.Infer())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to insert new user: %w", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// found => update the entry
|
|
|
|
foundPassword.PasswordHash = hashed
|
|
|
|
_, err = foundPassword.Update(ctx, tx, boil.Whitelist("password_hash"))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to update password for member: %w", err)
|
|
|
|
}
|
|
|
|
}
|
2021-02-08 16:47:42 +00:00
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-12 12:41:47 +00:00
|
|
|
func (af AuthFallback) SetPasswordWithToken(ctx context.Context, resetToken string, password string) error {
|
|
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
2021-02-08 16:47:42 +00:00
|
|
|
if err != nil {
|
2021-05-11 09:05:17 +00:00
|
|
|
return fmt.Errorf("auth/fallback: failed to hash password for member")
|
|
|
|
}
|
|
|
|
|
|
|
|
hashedTok, err := getHashedToken(resetToken)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid password reset token")
|
2021-02-08 16:47:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
// this is a silly upsert construction which sqlboiler-sqlite doesnt support nativly
|
|
|
|
return transact(af.db, func(tx *sql.Tx) error {
|
|
|
|
// make sure its a valid one and load it
|
|
|
|
resetEntry, err := models.FallbackResetTokens(qm.Where("active = true and hashed_token = ?", hashedTok)).One(ctx, tx)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return fmt.Errorf("could not find the reset-token")
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// see that there is a password entry for the member in the reset entry
|
|
|
|
foundPassword, err := models.FallbackPasswords(qm.Where("member_id = ?", resetEntry.ForMember)).One(ctx, tx)
|
|
|
|
if err != nil {
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// not found => insert new entry
|
|
|
|
var newPasswordEntry models.FallbackPassword
|
|
|
|
newPasswordEntry.PasswordHash = hashed
|
|
|
|
newPasswordEntry.MemberID = resetEntry.ForMember
|
|
|
|
err = newPasswordEntry.Insert(ctx, tx, boil.Infer())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to insert new fallback password for member: %w", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// found it => update the entry
|
|
|
|
foundPassword.PasswordHash = hashed
|
|
|
|
_, err = foundPassword.Update(ctx, tx, boil.Whitelist("password_hash"))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to update password for member: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// finally, invalidate the token
|
|
|
|
resetEntry.Active = false
|
|
|
|
_, err = resetEntry.Update(ctx, tx, boil.Whitelist("active"))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("auth/fallback: failed to invalidate the reset entry: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (af AuthFallback) CreateResetToken(ctx context.Context, createdByMember, forMember int64) (string, error) {
|
|
|
|
var newResetToken = models.FallbackResetToken{
|
|
|
|
CreatedBy: createdByMember,
|
|
|
|
ForMember: forMember,
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenBytes := make([]byte, inviteTokenLength)
|
|
|
|
|
|
|
|
err := transact(af.db, func(tx *sql.Tx) error {
|
|
|
|
|
|
|
|
inserted := false
|
|
|
|
trying:
|
|
|
|
for tries := 100; tries > 0; tries-- {
|
|
|
|
// generate an invite code
|
|
|
|
rand.Read(tokenBytes)
|
|
|
|
|
|
|
|
// hash the binary of the token for storage
|
|
|
|
h := sha256.New()
|
|
|
|
h.Write(tokenBytes)
|
|
|
|
newResetToken.HashedToken = fmt.Sprintf("%x", h.Sum(nil))
|
|
|
|
|
|
|
|
// insert the new invite
|
|
|
|
err := newResetToken.Insert(ctx, tx, boil.Infer())
|
|
|
|
if err != nil {
|
|
|
|
var sqlErr sqlite3.Error
|
|
|
|
if errors.As(err, &sqlErr) && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
|
|
|
// generated an existing token, retry
|
|
|
|
continue trying
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
inserted = true
|
|
|
|
break // no error means it worked!
|
|
|
|
}
|
|
|
|
|
|
|
|
if !inserted {
|
|
|
|
return errors.New("admindb: failed to generate an invite token in a reasonable amount of time")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
2021-02-08 16:47:42 +00:00
|
|
|
if err != nil {
|
2021-05-11 09:05:17 +00:00
|
|
|
return "", err
|
2021-02-08 16:47:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-11 09:05:17 +00:00
|
|
|
return base64.URLEncoding.EncodeToString(tokenBytes), nil
|
|
|
|
}
|
|
|
|
|
2021-06-15 14:24:06 +00:00
|
|
|
// since reset tokens are marked as invalid so that the code can't be generated twice,
|
2021-05-11 09:05:17 +00:00
|
|
|
// they need to be deleted periodically.
|
|
|
|
func deleteConsumedResetTokens(tx boil.ContextExecutor) error {
|
|
|
|
_, err := models.FallbackResetTokens(qm.Where("active = false")).DeleteAll(context.Background(), tx)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("admindb: failed to delete used reset tokens: %w", err)
|
|
|
|
}
|
2021-03-18 16:49:52 +00:00
|
|
|
return nil
|
2021-02-08 16:47:42 +00:00
|
|
|
}
|