feat: add audit logs

This commit is contained in:
Frederic Jahn
2022-08-18 16:53:34 +02:00
parent 7557c2e1e7
commit f02bccb685
23 changed files with 663 additions and 115 deletions

View File

@ -17,6 +17,7 @@ COPY crypto crypto/
COPY dto dto/ COPY dto dto/
COPY session session/ COPY session session/
COPY mail mail/ COPY mail mail/
COPY audit_log audit_log/
# Build # Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o hanko main.go RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o hanko main.go

View File

@ -0,0 +1,70 @@
package auditlog
import (
"fmt"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
)
type Client interface {
Create(echo.Context, models.AuditLogType, *models.User, error) error
}
type client struct {
persister persistence.Persister
enableStoring bool
}
func NewClient(persister persistence.Persister, config config.AuditLog) Client {
return &client{
persister: persister,
enableStoring: config.EnableStoring,
}
}
func (c *client) Create(context echo.Context, auditLogType models.AuditLogType, user *models.User, logError error) error {
var err error = nil
if c.enableStoring {
err = c.store(context, auditLogType, user, logError)
if err != nil {
return err
}
}
// TODO: log each auditLogType (logrus, zerolog, ...)
return nil
}
func (c *client) store(context echo.Context, auditLogType models.AuditLogType, user *models.User, logError error) error {
id, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("failed to create id: %w", err)
}
var userId *uuid.UUID = nil
var userEmail = ""
if user != nil {
userId = &user.ID
userEmail = user.Email
}
errString := ""
if logError != nil {
// check if error is not nil, because else the string (formatted with fmt.Sprintf) would not be empty but look like this: `%!s(<nil>)`
errString = fmt.Sprintf("%s", logError)
}
e := models.AuditLog{
ID: id,
Type: auditLogType,
Error: errString,
MetaHttpRequestId: context.Response().Header().Get(echo.HeaderXRequestID),
MetaUserAgent: context.Request().UserAgent(),
MetaSourceIp: context.RealIP(),
ActorUserId: userId,
ActorEmail: userEmail,
}
return c.persister.GetAuditLogPersister().Create(e)
}

View File

@ -22,6 +22,7 @@ type Config struct {
Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"` Secrets Secrets `yaml:"secrets" json:"secrets" koanf:"secrets"`
Service Service `yaml:"service" json:"service" koanf:"service"` Service Service `yaml:"service" json:"service" koanf:"service"`
Session Session `yaml:"session" json:"session" koanf:"session"` Session Session `yaml:"session" json:"session" koanf:"session"`
AuditLog AuditLog `yaml:"audit_log" json:"audit_log" koanf:"audit_log"`
} }
func Load(cfgFile *string) (*Config, error) { func Load(cfgFile *string) (*Config, error) {
@ -315,3 +316,7 @@ func (s *Session) Validate() error {
} }
return nil return nil
} }
type AuditLog struct {
EnableStoring bool `yaml:"enable_storing" json:"enable_storing" koanf:"enable_storing"`
}

View File

@ -0,0 +1,41 @@
package handler
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence"
"net/http"
)
type AuditLogHandler struct {
persister persistence.Persister
}
func NewAuditLogHandler(persister persistence.Persister) *AuditLogHandler {
return &AuditLogHandler{
persister: persister,
}
}
type AuditLogListRequest struct {
Page int `query:"page"`
PerPage int `query:"per_page"`
}
func (h AuditLogHandler) List(c echo.Context) error {
var request AuditLogListRequest
err := (&echo.DefaultBinder{}).BindQueryParams(c, &request)
if err != nil {
return dto.ToHttpError(err)
}
auditLogs, err := h.persister.GetAuditLogPersister().List(request.Page, request.PerPage)
if err != nil {
return fmt.Errorf("failed to get list of audit logs: %w", err)
}
// TODO: maybe change return format to more structured json
return c.JSON(http.StatusOK, auditLogs)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/gobuffalo/pop/v6" "github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto" "github.com/teamhanko/hanko/backend/crypto"
"github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/dto"
@ -29,11 +30,12 @@ type PasscodeHandler struct {
TTL int TTL int
sessionManager session.Manager sessionManager session.Manager
cfg *config.Config cfg *config.Config
auditLogClient auditlog.Client
} }
var maxPasscodeTries = 3 var maxPasscodeTries = 3
func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, mailer mail.Mailer) (*PasscodeHandler, error) { func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, mailer mail.Mailer, auditLogClient auditlog.Client) (*PasscodeHandler, error) {
renderer, err := mail.NewRenderer() renderer, err := mail.NewRenderer()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create new renderer: %w", err) return nil, fmt.Errorf("failed to create new renderer: %w", err)
@ -48,6 +50,7 @@ func NewPasscodeHandler(cfg *config.Config, persister persistence.Persister, ses
TTL: cfg.Passcode.TTL, TTL: cfg.Passcode.TTL,
sessionManager: sessionManager, sessionManager: sessionManager,
cfg: cfg, cfg: cfg,
auditLogClient: auditLogClient,
}, nil }, nil
} }
@ -71,6 +74,10 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to get user: %w", err)
} }
if user == nil { if user == nil {
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginInitFailed, nil, fmt.Errorf("unknown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found")) return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found"))
} }
@ -128,6 +135,11 @@ func (h *PasscodeHandler) Init(c echo.Context) error {
return fmt.Errorf("failed to send passcode: %w", err) return fmt.Errorf("failed to send passcode: %w", err)
} }
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginInitSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, dto.PasscodeReturn{ return c.JSON(http.StatusOK, dto.PasscodeReturn{
Id: passcodeId.String(), Id: passcodeId.String(),
TTL: h.TTL, TTL: h.TTL,
@ -151,7 +163,7 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return dto.NewHTTPError(http.StatusBadRequest, "failed to parse passcodeId as uuid").SetInternal(err) return dto.NewHTTPError(http.StatusBadRequest, "failed to parse passcodeId as uuid").SetInternal(err)
} }
// only if an internal server occurs the transaction should be rolled back // only if an internal server error occurs the transaction should be rolled back
var businessError error var businessError error
transactionError := h.persister.Transaction(func(tx *pop.Connection) error { transactionError := h.persister.Transaction(func(tx *pop.Connection) error {
passcodePersister := h.persister.GetPasscodePersisterWithConnection(tx) passcodePersister := h.persister.GetPasscodePersisterWithConnection(tx)
@ -161,12 +173,25 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return fmt.Errorf("failed to get passcode: %w", err) return fmt.Errorf("failed to get passcode: %w", err)
} }
if passcode == nil { if passcode == nil {
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginFailed, nil, fmt.Errorf("unknown passcode"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
businessError = dto.NewHTTPError(http.StatusNotFound, "passcode not found") businessError = dto.NewHTTPError(http.StatusNotFound, "passcode not found")
return nil return nil
} }
user, err := userPersister.Get(passcode.UserId)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
lastVerificationTime := passcode.CreatedAt.Add(time.Duration(passcode.Ttl) * time.Second) lastVerificationTime := passcode.CreatedAt.Add(time.Duration(passcode.Ttl) * time.Second)
if lastVerificationTime.Before(startTime) { if lastVerificationTime.Before(startTime) {
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginFailed, user, fmt.Errorf("timed out passcode"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
businessError = dto.NewHTTPError(http.StatusRequestTimeout, "passcode request timed out").SetInternal(errors.New(fmt.Sprintf("createdAt: %s -> lastVerificationTime: %s", passcode.CreatedAt, lastVerificationTime))) // TODO: maybe we should use BadRequest, because RequestTimeout might be to technical and can refer to different error businessError = dto.NewHTTPError(http.StatusRequestTimeout, "passcode request timed out").SetInternal(errors.New(fmt.Sprintf("createdAt: %s -> lastVerificationTime: %s", passcode.CreatedAt, lastVerificationTime))) // TODO: maybe we should use BadRequest, because RequestTimeout might be to technical and can refer to different error
return nil return nil
} }
@ -180,6 +205,10 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to delete passcode: %w", err) return fmt.Errorf("failed to delete passcode: %w", err)
} }
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginFailed, user, fmt.Errorf("max attempts reached"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
businessError = dto.NewHTTPError(http.StatusGone, "max attempts reached") businessError = dto.NewHTTPError(http.StatusGone, "max attempts reached")
return nil return nil
} }
@ -189,6 +218,10 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return fmt.Errorf("failed to update passcode: %w", err) return fmt.Errorf("failed to update passcode: %w", err)
} }
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginFailed, user, fmt.Errorf("passcode invalid"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
businessError = dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("passcode invalid")) businessError = dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("passcode invalid"))
return nil return nil
} }
@ -198,11 +231,6 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
return fmt.Errorf("failed to delete passcode: %w", err) return fmt.Errorf("failed to delete passcode: %w", err)
} }
user, err := userPersister.Get(passcode.UserId)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if !user.Verified { if !user.Verified {
user.Verified = true user.Verified = true
err = userPersister.Update(*user) err = userPersister.Update(*user)
@ -227,6 +255,11 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {
c.Response().Header().Set("X-Auth-Token", token) c.Response().Header().Set("X-Auth-Token", token)
} }
err = h.auditLogClient.Create(c, models.AuditLogPasscodeLoginSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, dto.PasscodeReturn{ return c.JSON(http.StatusOK, dto.PasscodeReturn{
Id: passcode.ID.String(), Id: passcode.ID.String(),
TTL: passcode.Ttl, TTL: passcode.Ttl,

View File

@ -19,13 +19,13 @@ import (
) )
func TestNewPasscodeHandler(t *testing.T) { func TestNewPasscodeHandler(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(nil, nil, nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(nil, nil, nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, passcodeHandler) assert.NotEmpty(t, passcodeHandler)
} }
func TestPasscodeHandler_Init(t *testing.T) { func TestPasscodeHandler_Init(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, nil, nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, nil, nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeInitRequest{ body := dto.PasscodeInitRequest{
@ -47,7 +47,7 @@ func TestPasscodeHandler_Init(t *testing.T) {
} }
func TestPasscodeHandler_Init_UnknownUserId(t *testing.T) { func TestPasscodeHandler_Init_UnknownUserId(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, nil, nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, nil, nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeInitRequest{ body := dto.PasscodeInitRequest{
@ -71,7 +71,7 @@ func TestPasscodeHandler_Init_UnknownUserId(t *testing.T) {
} }
func TestPasscodeHandler_Finish(t *testing.T) { func TestPasscodeHandler_Finish(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeFinishRequest{ body := dto.PasscodeFinishRequest{
@ -94,7 +94,7 @@ func TestPasscodeHandler_Finish(t *testing.T) {
} }
func TestPasscodeHandler_Finish_WrongCode(t *testing.T) { func TestPasscodeHandler_Finish_WrongCode(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeFinishRequest{ body := dto.PasscodeFinishRequest{
@ -119,7 +119,7 @@ func TestPasscodeHandler_Finish_WrongCode(t *testing.T) {
} }
func TestPasscodeHandler_Finish_WrongCode_3_Times(t *testing.T) { func TestPasscodeHandler_Finish_WrongCode_3_Times(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeFinishRequest{ body := dto.PasscodeFinishRequest{
@ -153,7 +153,7 @@ func TestPasscodeHandler_Finish_WrongCode_3_Times(t *testing.T) {
} }
func TestPasscodeHandler_Finish_WrongId(t *testing.T) { func TestPasscodeHandler_Finish_WrongId(t *testing.T) {
passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil), sessionManager{}, mailer{}) passcodeHandler, err := NewPasscodeHandler(&config.Config{}, test.NewPersister(users, passcodes(), nil, nil, nil, nil, nil), sessionManager{}, mailer{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
body := dto.PasscodeFinishRequest{ body := dto.PasscodeFinishRequest{

View File

@ -7,6 +7,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt" "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/persistence"
@ -21,13 +22,15 @@ type PasswordHandler struct {
persister persistence.Persister persister persistence.Persister
sessionManager session.Manager sessionManager session.Manager
cfg *config.Config cfg *config.Config
auditLogClient auditlog.Client
} }
func NewPasswordHandler(persister persistence.Persister, sessionManager session.Manager, cfg *config.Config) *PasswordHandler { func NewPasswordHandler(persister persistence.Persister, sessionManager session.Manager, cfg *config.Config, auditLogClient auditlog.Client) *PasswordHandler {
return &PasswordHandler{ return &PasswordHandler{
persister: persister, persister: persister,
sessionManager: sessionManager, sessionManager: sessionManager,
cfg: cfg, cfg: cfg,
auditLogClient: auditLogClient,
} }
} }
@ -56,28 +59,45 @@ func (h *PasswordHandler) Set(c echo.Context) error {
return dto.NewHTTPError(http.StatusBadRequest, "failed to parse userId as uuid").SetInternal(err) return dto.NewHTTPError(http.StatusBadRequest, "failed to parse userId as uuid").SetInternal(err)
} }
user, err := h.persister.GetUserPersister().Get(uuid.FromStringOrNil(body.UserID))
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
pwBytes := []byte(body.Password) pwBytes := []byte(body.Password)
if utf8.RuneCountInString(body.Password) < h.cfg.Password.MinPasswordLength { // use utf8.RuneCountInString, so utf8 characters would count as 1 if utf8.RuneCountInString(body.Password) < h.cfg.Password.MinPasswordLength { // use utf8.RuneCountInString, so utf8 characters would count as 1
err = h.auditLogClient.Create(c, models.AuditLogPasswordSetFailed, user, fmt.Errorf("password too short"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters long", h.cfg.Password.MinPasswordLength)) return dto.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters long", h.cfg.Password.MinPasswordLength))
} }
if len(pwBytes) > 72 { if len(pwBytes) > 72 {
err = h.auditLogClient.Create(c, models.AuditLogPasswordSetFailed, user, fmt.Errorf("password too long"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "password must not be longer than 72 bytes") return dto.NewHTTPError(http.StatusBadRequest, "password must not be longer than 72 bytes")
} }
return h.persister.Transaction(func(tx *pop.Connection) error { if user == nil {
user, err := h.persister.GetUserPersisterWithConnection(tx).Get(uuid.FromStringOrNil(body.UserID)) err = h.auditLogClient.Create(c, models.AuditLogPasswordSetFailed, user, fmt.Errorf("unknown user: %s", body.UserID))
if err != nil { if err != nil {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to create audit log: %w", err)
} }
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(fmt.Sprintf("user %s not found ", sessionUserId)))
}
if user == nil { if sessionUserId != user.ID {
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(fmt.Sprintf("user %s not found ", sessionUserId))) err = h.auditLogClient.Create(c, models.AuditLogPasswordSetFailed, user, fmt.Errorf("wrong user: expected %s -> got %s", sessionUserId, user.ID))
} if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
if sessionUserId != user.ID {
return dto.NewHTTPError(http.StatusForbidden).SetInternal(errors.New(fmt.Sprintf("session.userId %s tried to set password credentials for body.userId %s", sessionUserId, user.ID)))
} }
return dto.NewHTTPError(http.StatusForbidden).SetInternal(errors.New(fmt.Sprintf("session.userId %s tried to set password credentials for body.userId %s", sessionUserId, user.ID)))
}
return h.persister.Transaction(func(tx *pop.Connection) error {
pwPersister := h.persister.GetPasswordCredentialPersisterWithConnection(tx) pwPersister := h.persister.GetPasswordCredentialPersisterWithConnection(tx)
pw, err := pwPersister.GetByUserID(user.ID) pw, err := pwPersister.GetByUserID(user.ID)
if err != nil { if err != nil {
@ -99,6 +119,10 @@ func (h *PasswordHandler) Set(c echo.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to create password: %w", err) return fmt.Errorf("failed to create password: %w", err)
} else { } else {
err = h.auditLogClient.Create(c, models.AuditLogPasswordSetSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusCreated, nil) return c.JSON(http.StatusCreated, nil)
} }
} else { } else {
@ -107,6 +131,10 @@ func (h *PasswordHandler) Set(c echo.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to set password: %w", err) return fmt.Errorf("failed to set password: %w", err)
} else { } else {
err = h.auditLogClient.Create(c, models.AuditLogPasswordSetSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, nil) return c.JSON(http.StatusOK, nil)
} }
} }
@ -128,13 +156,38 @@ func (h *PasswordHandler) Login(c echo.Context) error {
return dto.ToHttpError(err) return dto.ToHttpError(err)
} }
userId, err := uuid.FromString(body.UserId)
if err != nil {
return dto.NewHTTPError(http.StatusBadRequest, "user_id is not a uuid").SetInternal(err)
}
user, err := h.persister.GetUserPersister().Get(userId)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
err = h.auditLogClient.Create(c, models.AuditLogPasswordLoginFailed, nil, fmt.Errorf("unknown user: %s", userId))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found"))
}
pwBytes := []byte(body.Password) pwBytes := []byte(body.Password)
if len(pwBytes) > 72 { if len(pwBytes) > 72 {
err = h.auditLogClient.Create(c, models.AuditLogPasswordLoginFailed, user, errors.New("password too long"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "password must not be longer than 72 bytes") return dto.NewHTTPError(http.StatusBadRequest, "password must not be longer than 72 bytes")
} }
pw, err := h.persister.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(body.UserId)) pw, err := h.persister.GetPasswordCredentialPersister().GetByUserID(uuid.FromStringOrNil(body.UserId))
if pw == nil { if pw == nil {
err = h.auditLogClient.Create(c, models.AuditLogPasswordLoginFailed, user, fmt.Errorf("user has no password credential"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(fmt.Sprintf("no password credential found for: %s", body.UserId))) return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New(fmt.Sprintf("no password credential found for: %s", body.UserId)))
} }
@ -143,6 +196,10 @@ func (h *PasswordHandler) Login(c echo.Context) error {
} }
if err = bcrypt.CompareHashAndPassword([]byte(pw.Password), pwBytes); err != nil { if err = bcrypt.CompareHashAndPassword([]byte(pw.Password), pwBytes); err != nil {
err = h.auditLogClient.Create(c, models.AuditLogPasswordLoginFailed, user, fmt.Errorf("password hash not equal"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(err) return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(err)
} }
@ -162,5 +219,10 @@ func (h *PasswordHandler) Login(c echo.Context) error {
c.Response().Header().Set("X-Auth-Token", token) c.Response().Header().Set("X-Auth-Token", token)
} }
err = h.auditLogClient.Create(c, models.AuditLogPasswordLoginSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, nil) return c.JSON(http.StatusOK, nil)
} }

View File

@ -47,8 +47,8 @@ func TestPasswordHandler_Set_Create(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}) p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
if assert.NoError(t, handler.Set(c)) { if assert.NoError(t, handler.Set(c)) {
assert.Equal(t, http.StatusCreated, rec.Code) assert.Equal(t, http.StatusCreated, rec.Code)
@ -82,8 +82,8 @@ func TestPasswordHandler_Set_Create_PasswordTooShort(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}) p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}, test.NewAuditLogClient())
err = handler.Set(c) err = handler.Set(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -119,8 +119,8 @@ func TestPasswordHandler_Set_Create_PasswordTooLong(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}) p := test.NewPersister(users, nil, nil, nil, nil, []models.PasswordCredential{}, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{Password: config.Password{MinPasswordLength: 8}}, test.NewAuditLogClient())
err = handler.Set(c) err = handler.Set(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -172,8 +172,8 @@ func TestPasswordHandler_Set_Update(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, passwords) p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
if assert.NoError(t, handler.Set(c)) { if assert.NoError(t, handler.Set(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
@ -197,8 +197,8 @@ func TestPasswordHandler_Set_UserNotFound(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}) p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
err = handler.Set(c) err = handler.Set(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -250,8 +250,8 @@ func TestPasswordHandler_Set_TokenHasWrongSubject(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, passwords) p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
err = handler.Set(c) err = handler.Set(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -275,8 +275,8 @@ func TestPasswordHandler_Set_BadRequestBody(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
err = handler.Set(c) err = handler.Set(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -322,8 +322,8 @@ func TestPasswordHandler_Login(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, passwords) p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
if assert.NoError(t, handler.Login(c)) { if assert.NoError(t, handler.Login(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
@ -375,8 +375,8 @@ func TestPasswordHandler_Login_WrongPassword(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, passwords) p := test.NewPersister(users, nil, nil, nil, nil, passwords, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
err = handler.Login(c) err = handler.Login(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -395,8 +395,8 @@ func TestPasswordHandler_Login_NonExistingUser(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}) p := test.NewPersister([]models.User{}, nil, nil, nil, nil, []models.PasswordCredential{}, nil)
handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}) handler := NewPasswordHandler(p, sessionManager{}, &config.Config{}, test.NewAuditLogClient())
err := handler.Login(c) err := handler.Login(c)
if assert.Error(t, err) { if assert.Error(t, err) {

View File

@ -7,6 +7,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt" "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence" "github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models" "github.com/teamhanko/hanko/backend/persistence/models"
@ -15,11 +16,15 @@ import (
) )
type UserHandler struct { type UserHandler struct {
persister persistence.Persister persister persistence.Persister
auditLogClient auditlog.Client
} }
func NewUserHandler(persister persistence.Persister) *UserHandler { func NewUserHandler(persister persistence.Persister, auditLogClient auditlog.Client) *UserHandler {
return &UserHandler{persister: persister} return &UserHandler{
persister: persister,
auditLogClient: auditLogClient,
}
} }
type UserCreateBody struct { type UserCreateBody struct {
@ -54,6 +59,8 @@ func (h *UserHandler) Create(c echo.Context) error {
return fmt.Errorf("failed to store user: %w", err) return fmt.Errorf("failed to store user: %w", err)
} }
_ = h.auditLogClient.Create(c, models.AuditLogUserCreated, &newUser, nil) // TODO: what to do on error
return c.JSON(http.StatusOK, newUser) return c.JSON(http.StatusOK, newUser)
}) })
} }

View File

@ -35,7 +35,7 @@ func TestUserHandlerAdmin_Delete(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues(userId.String()) c.SetParamValues(userId.String())
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
if assert.NoError(t, handler.Delete(c)) { if assert.NoError(t, handler.Delete(c)) {
@ -52,7 +52,7 @@ func TestUserHandlerAdmin_Delete_InvalidUserId(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues("invalidId") c.SetParamValues("invalidId")
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Delete(c) err := handler.Delete(c)
@ -72,7 +72,7 @@ func TestUserHandlerAdmin_Delete_UnknownUserId(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues(userId.String()) c.SetParamValues(userId.String())
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Delete(c) err := handler.Delete(c)
@ -104,7 +104,7 @@ func TestUserHandlerAdmin_Patch(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues(userId.String()) c.SetParamValues(userId.String())
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
if assert.NoError(t, handler.Patch(c)) { if assert.NoError(t, handler.Patch(c)) {
@ -124,7 +124,7 @@ func TestUserHandlerAdmin_Patch_InvalidUserIdAndEmail(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues("invalidUserId") c.SetParamValues("invalidUserId")
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Patch(c) err := handler.Patch(c)
@ -167,7 +167,7 @@ func TestUserHandlerAdmin_Patch_EmailNotAvailable(t *testing.T) {
c.SetParamNames("id") c.SetParamNames("id")
c.SetParamValues(users[0].ID.String()) c.SetParamValues(users[0].ID.String())
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Patch(c) err := handler.Patch(c)
@ -200,7 +200,7 @@ func TestUserHandlerAdmin_Patch_UnknownUserId(t *testing.T) {
unknownUserId, _ := uuid.NewV4() unknownUserId, _ := uuid.NewV4()
c.SetParamValues(unknownUserId.String()) c.SetParamValues(unknownUserId.String())
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Patch(c) err := handler.Patch(c)
@ -233,7 +233,7 @@ func TestUserHandlerAdmin_Patch_InvalidJson(t *testing.T) {
unknownUserId, _ := uuid.NewV4() unknownUserId, _ := uuid.NewV4()
c.SetParamValues(unknownUserId.String()) c.SetParamValues(unknownUserId.String())
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.Patch(c) err := handler.Patch(c)
@ -271,7 +271,7 @@ func TestUserHandlerAdmin_List(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
if assert.NoError(t, handler.List(c)) { if assert.NoError(t, handler.List(c)) {
@ -314,7 +314,7 @@ func TestUserHandlerAdmin_List_Pagination(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
if assert.NoError(t, handler.List(c)) { if assert.NoError(t, handler.List(c)) {
@ -336,7 +336,7 @@ func TestUserHandlerAdmin_List_NoUsers(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
if assert.NoError(t, handler.List(c)) { if assert.NoError(t, handler.List(c)) {
@ -357,7 +357,7 @@ func TestUserHandlerAdmin_List_InvalidPaginationParam(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandlerAdmin(p) handler := NewUserHandlerAdmin(p)
err := handler.List(c) err := handler.List(c)

View File

@ -42,8 +42,8 @@ func TestUserHandler_Create(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.Create(c)) { if assert.NoError(t, handler.Create(c)) {
user := models.User{} user := models.User{}
@ -78,8 +78,8 @@ func TestUserHandler_Create_CaseInsensitive(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.Create(c)) { if assert.NoError(t, handler.Create(c)) {
user := models.User{} user := models.User{}
@ -113,8 +113,8 @@ func TestUserHandler_Create_UserExists(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err = handler.Create(c) err = handler.Create(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -146,8 +146,8 @@ func TestUserHandler_Create_UserExists_CaseInsensitive(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err = handler.Create(c) err = handler.Create(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -165,8 +165,8 @@ func TestUserHandler_Create_InvalidEmail(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err := handler.Create(c) err := handler.Create(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -184,8 +184,8 @@ func TestUserHandler_Create_EmailMissing(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err := handler.Create(c) err := handler.Create(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -220,8 +220,8 @@ func TestUserHandler_Get(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.Get(c)) { if assert.NoError(t, handler.Get(c)) {
assert.Equal(t, rec.Code, http.StatusOK) assert.Equal(t, rec.Code, http.StatusOK)
@ -270,8 +270,8 @@ func TestUserHandler_GetUserWithWebAuthnCredential(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.Get(c)) { if assert.NoError(t, handler.Get(c)) {
assert.Equal(t, rec.Code, http.StatusOK) assert.Equal(t, rec.Code, http.StatusOK)
@ -295,8 +295,8 @@ func TestUserHandler_Get_InvalidUserId(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err = handler.Get(c) err = handler.Get(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -313,8 +313,8 @@ func TestUserHandler_GetUserIdByEmail_InvalidEmail(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err := handler.GetUserIdByEmail(c) err := handler.GetUserIdByEmail(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -330,8 +330,8 @@ func TestUserHandler_GetUserIdByEmail_InvalidJson(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
assert.Error(t, handler.GetUserIdByEmail(c)) assert.Error(t, handler.GetUserIdByEmail(c))
} }
@ -344,8 +344,8 @@ func TestUserHandler_GetUserIdByEmail_UserNotFound(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
err := handler.GetUserIdByEmail(c) err := handler.GetUserIdByEmail(c)
if assert.Error(t, err) { if assert.Error(t, err) {
@ -372,8 +372,8 @@ func TestUserHandler_GetUserIdByEmail(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.GetUserIdByEmail(c)) { if assert.NoError(t, handler.GetUserIdByEmail(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
@ -406,8 +406,8 @@ func TestUserHandler_GetUserIdByEmail_CaseInsensitive(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.GetUserIdByEmail(c)) { if assert.NoError(t, handler.GetUserIdByEmail(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
@ -436,8 +436,8 @@ func TestUserHandler_Me(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, nil, nil) p := test.NewPersister(users, nil, nil, nil, nil, nil, nil)
handler := NewUserHandler(p) handler := NewUserHandler(p, test.NewAuditLogClient())
if assert.NoError(t, handler.Me(c)) { if assert.NoError(t, handler.Me(c)) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)

View File

@ -10,6 +10,7 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt" "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/dto/intern" "github.com/teamhanko/hanko/backend/dto/intern"
@ -24,10 +25,11 @@ type WebauthnHandler struct {
webauthn *webauthn.WebAuthn webauthn *webauthn.WebAuthn
sessionManager session.Manager sessionManager session.Manager
cfg *config.Config cfg *config.Config
auditLogClient auditlog.Client
} }
// NewWebauthnHandler creates a new handler which handles all webauthn related routes // NewWebauthnHandler creates a new handler which handles all webauthn related routes
func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager) (*WebauthnHandler, error) { func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, sessionManager session.Manager, auditLogClient auditlog.Client) (*WebauthnHandler, error) {
f := false f := false
wa, err := webauthn.New(&webauthn.Config{ wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName, RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName,
@ -52,6 +54,7 @@ func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, ses
webauthn: wa, webauthn: wa,
sessionManager: sessionManager, sessionManager: sessionManager,
cfg: cfg, cfg: cfg,
auditLogClient: auditLogClient,
}, nil }, nil
} }
@ -65,11 +68,15 @@ func (h *WebauthnHandler) BeginRegistration(c echo.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to parse userId from JWT subject:%w", err) return fmt.Errorf("failed to parse userId from JWT subject:%w", err)
} }
webauthnUser, err := h.getWebauthnUser(h.persister.GetConnection(), uId) webauthnUser, user, err := h.getWebauthnUser(h.persister.GetConnection(), uId)
if err != nil { if err != nil {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to get user: %w", err)
} }
if webauthnUser == nil { if webauthnUser == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationInitFailed, nil, fmt.Errorf("unknown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "user not found").SetInternal(errors.New(fmt.Sprintf("user %s not found ", uId))) return dto.NewHTTPError(http.StatusBadRequest, "user not found").SetInternal(errors.New(fmt.Sprintf("user %s not found ", uId)))
} }
@ -95,6 +102,11 @@ func (h *WebauthnHandler) BeginRegistration(c echo.Context) error {
return fmt.Errorf("failed to store creation options session data: %w", err) return fmt.Errorf("failed to store creation options session data: %w", err)
} }
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationInitSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, options) return c.JSON(http.StatusOK, options)
} }
@ -121,24 +133,40 @@ func (h *WebauthnHandler) FinishRegistration(c echo.Context) error {
} }
if sessionData == nil { if sessionData == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationFailed, nil, fmt.Errorf("received unkown challenge"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found")) return dto.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found"))
} }
if sessionToken.Subject() != sessionData.UserId.String() { if sessionToken.Subject() != sessionData.UserId.String() {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationFailed, nil, fmt.Errorf("user session does not match sessionData subject"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("userId in webauthn.sessionData does not match user session")) return dto.NewHTTPError(http.StatusBadRequest, "Stored challenge and received challenge do not match").SetInternal(errors.New("userId in webauthn.sessionData does not match user session"))
} }
webauthnUser, err := h.getWebauthnUser(tx, sessionData.UserId) webauthnUser, user, err := h.getWebauthnUser(tx, sessionData.UserId)
if err != nil { if err != nil {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to get user: %w", err)
} }
if webauthnUser == nil { if webauthnUser == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationFailed, nil, fmt.Errorf("unkown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found")) return dto.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("user not found"))
} }
credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), request) credential, err := h.webauthn.CreateCredential(webauthnUser, *intern.WebauthnSessionDataFromModel(sessionData), request)
if err != nil { if err != nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationFailed, user, fmt.Errorf("attestation validation failed"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "Failed to validate attestation").SetInternal(err) return dto.NewHTTPError(http.StatusBadRequest, "Failed to validate attestation").SetInternal(err)
} }
@ -153,6 +181,11 @@ func (h *WebauthnHandler) FinishRegistration(c echo.Context) error {
c.Logger().Errorf("failed to delete attestation session data: %w", err) c.Logger().Errorf("failed to delete attestation session data: %w", err)
} }
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnRegistrationSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, map[string]string{"credential_id": model.ID, "user_id": webauthnUser.UserId.String()}) return c.JSON(http.StatusOK, map[string]string{"credential_id": model.ID, "user_id": webauthnUser.UserId.String()})
}) })
} }
@ -171,17 +204,27 @@ func (h *WebauthnHandler) BeginAuthentication(c echo.Context) error {
var options *protocol.CredentialAssertion var options *protocol.CredentialAssertion
var sessionData *webauthn.SessionData var sessionData *webauthn.SessionData
var user *models.User
if request.UserID != nil { if request.UserID != nil {
// non discoverable login initialization // non discoverable login initialization
userId, err := uuid.FromString(*request.UserID) userId, err := uuid.FromString(*request.UserID)
if err != nil { if err != nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf("user_id is not a uuid"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "failed to parse UserID as uuid").SetInternal(err) return dto.NewHTTPError(http.StatusBadRequest, "failed to parse UserID as uuid").SetInternal(err)
} }
webauthnUser, err := h.getWebauthnUser(h.persister.GetConnection(), userId) var webauthnUser *intern.WebauthnUser
webauthnUser, user, err = h.getWebauthnUser(h.persister.GetConnection(), userId) // TODO:
if err != nil { if err != nil {
return dto.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf("failed to get user: %w", err)) return dto.NewHTTPError(http.StatusInternalServerError).SetInternal(fmt.Errorf("failed to get user: %w", err))
} }
if webauthnUser == nil { if webauthnUser == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationInitFailed, nil, fmt.Errorf("unkown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusBadRequest, "user not found") return dto.NewHTTPError(http.StatusBadRequest, "user not found")
} }
@ -211,6 +254,11 @@ func (h *WebauthnHandler) BeginAuthentication(c echo.Context) error {
options.Response.AllowedCredentials[i].Transport = nil options.Response.AllowedCredentials[i].Transport = nil
} }
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationInitSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, options) return c.JSON(http.StatusOK, options)
} }
@ -233,6 +281,10 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {
} }
if sessionData == nil { if sessionData == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationFailed, nil, fmt.Errorf("received unkown challenge"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found")) return dto.NewHTTPError(http.StatusUnauthorized, "Stored challenge and received challenge do not match").SetInternal(errors.New("sessionData not found"))
} }
@ -240,18 +292,23 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {
var credential *webauthn.Credential var credential *webauthn.Credential
var webauthnUser *intern.WebauthnUser var webauthnUser *intern.WebauthnUser
var user *models.User
if sessionData.UserId.IsNil() { if sessionData.UserId.IsNil() {
// Discoverable Login // Discoverable Login
userId, err := uuid.FromBytes(request.Response.UserHandle) userId, err := uuid.FromBytes(request.Response.UserHandle)
if err != nil { if err != nil {
return dto.NewHTTPError(http.StatusBadRequest, "failed to parse userHandle as uuid").SetInternal(err) return dto.NewHTTPError(http.StatusBadRequest, "failed to parse userHandle as uuid").SetInternal(err)
} }
webauthnUser, err = h.getWebauthnUser(tx, userId) webauthnUser, user, err = h.getWebauthnUser(tx, userId)
if err != nil { if err != nil {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to get user: %w", err)
} }
if webauthnUser == nil { if webauthnUser == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationFailed, nil, fmt.Errorf("unkown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found")) return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found"))
} }
@ -259,19 +316,31 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {
return webauthnUser, nil return webauthnUser, nil
}, *model, request) }, *model, request)
if err != nil { if err != nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationFailed, user, fmt.Errorf("assertion validation failed"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) return dto.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err)
} }
} else { } else {
// non discoverable Login // non discoverable Login
webauthnUser, err = h.getWebauthnUser(tx, sessionData.UserId) webauthnUser, user, err = h.getWebauthnUser(tx, sessionData.UserId)
if err != nil { if err != nil {
return fmt.Errorf("failed to get user: %w", err) return fmt.Errorf("failed to get user: %w", err)
} }
if webauthnUser == nil { if webauthnUser == nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationFailed, nil, fmt.Errorf("unkown user"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found")) return dto.NewHTTPError(http.StatusUnauthorized).SetInternal(errors.New("user not found"))
} }
credential, err = h.webauthn.ValidateLogin(webauthnUser, *model, request) credential, err = h.webauthn.ValidateLogin(webauthnUser, *model, request)
if err != nil { if err != nil {
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationFailed, user, fmt.Errorf("assertion validation failed"))
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return dto.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err) return dto.NewHTTPError(http.StatusUnauthorized, "failed to validate assertion").SetInternal(err)
} }
} }
@ -297,24 +366,29 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {
c.Response().Header().Set("X-Auth-Token", token) c.Response().Header().Set("X-Auth-Token", token)
} }
err = h.auditLogClient.Create(c, models.AuditLogWebAuthnAuthenticationSucceeded, user, nil)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return c.JSON(http.StatusOK, map[string]string{"credential_id": base64.RawURLEncoding.EncodeToString(credential.ID), "user_id": webauthnUser.UserId.String()}) return c.JSON(http.StatusOK, map[string]string{"credential_id": base64.RawURLEncoding.EncodeToString(credential.ID), "user_id": webauthnUser.UserId.String()})
}) })
} }
func (h WebauthnHandler) getWebauthnUser(connection *pop.Connection, userId uuid.UUID) (*intern.WebauthnUser, error) { func (h WebauthnHandler) getWebauthnUser(connection *pop.Connection, userId uuid.UUID) (*intern.WebauthnUser, *models.User, error) {
user, err := h.persister.GetUserPersisterWithConnection(connection).Get(userId) user, err := h.persister.GetUserPersisterWithConnection(connection).Get(userId)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err) return nil, nil, fmt.Errorf("failed to get user: %w", err)
} }
if user == nil { if user == nil {
return nil, nil return nil, nil, nil
} }
credentials, err := h.persister.GetWebauthnCredentialPersisterWithConnection(connection).GetFromUser(user.ID) credentials, err := h.persister.GetWebauthnCredentialPersisterWithConnection(connection).GetFromUser(user.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get webauthn credentials: %w", err) return nil, nil, fmt.Errorf("failed to get webauthn credentials: %w", err)
} }
return intern.NewWebauthnUser(*user, credentials), nil return intern.NewWebauthnUser(*user, credentials), user, nil
} }

View File

@ -23,8 +23,8 @@ var userId = "ec4ef049-5b88-4321-a173-21b0eff06a04"
var userIdBytes = []byte{0xec, 0x4e, 0xf0, 0x49, 0x5b, 0x88, 0x43, 0x21, 0xa1, 0x73, 0x21, 0xb0, 0xef, 0xf0, 0x6a, 0x4} var userIdBytes = []byte{0xec, 0x4e, 0xf0, 0x49, 0x5b, 0x88, 0x43, 0x21, 0xa1, 0x73, 0x21, 0xb0, 0xef, 0xf0, 0x6a, 0x4}
func TestNewWebauthnHandler(t *testing.T) { func TestNewWebauthnHandler(t *testing.T) {
p := test.NewPersister(nil, nil, nil, nil, nil, nil) p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}) handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogClient())
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, handler) assert.NotEmpty(t, handler)
} }
@ -39,8 +39,8 @@ func TestWebauthnHandler_BeginRegistration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, credentials, sessionData, nil) p := test.NewPersister(users, nil, nil, credentials, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}) handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
if assert.NoError(t, handler.BeginRegistration(c)) { if assert.NoError(t, handler.BeginRegistration(c)) {
@ -75,8 +75,8 @@ func TestWebauthnHandler_FinishRegistration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c.Set("session", token) c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, sessionData, nil) p := test.NewPersister(users, nil, nil, nil, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}) handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
if assert.NoError(t, handler.FinishRegistration(c)) { if assert.NoError(t, handler.FinishRegistration(c)) {
@ -106,8 +106,8 @@ func TestWebauthnHandler_BeginAuthentication(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, sessionData, nil) p := test.NewPersister(users, nil, nil, nil, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}) handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
if assert.NoError(t, handler.BeginAuthentication(c)) { if assert.NoError(t, handler.BeginAuthentication(c)) {
@ -138,8 +138,8 @@ func TestWebauthnHandler_FinishAuthentication(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := e.NewContext(req, rec) c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, credentials, sessionData, nil) p := test.NewPersister(users, nil, nil, credentials, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}) handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogClient())
require.NoError(t, err) require.NoError(t, err)
if assert.NoError(t, handler.FinishAuthentication(c)) { if assert.NoError(t, handler.FinishAuthentication(c)) {

View File

@ -0,0 +1,72 @@
package persistence
import (
"database/sql"
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence/models"
)
type AuditLogPersister interface {
Create(auditLog models.AuditLog) error
Get(id uuid.UUID) (*models.AuditLog, error)
List(page int, perPage int) ([]models.AuditLog, error)
Delete(auditLog models.AuditLog) error
}
type auditLogPersister struct {
db *pop.Connection
}
func NewAuditLogPersister(db *pop.Connection) AuditLogPersister {
return &auditLogPersister{db: db}
}
func (p *auditLogPersister) Create(auditLog models.AuditLog) error {
vErr, err := p.db.ValidateAndCreate(&auditLog)
if err != nil {
return fmt.Errorf("failed to store auditlog: %w", err)
}
if vErr != nil && vErr.HasAny() {
return fmt.Errorf("auditlog object validation failed: %w", vErr)
}
return nil
}
func (p *auditLogPersister) Get(id uuid.UUID) (*models.AuditLog, error) {
auditLog := models.AuditLog{}
err := p.db.Eager().Find(&auditLog, id)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get auditlog: %w", err)
}
return &auditLog, nil
}
func (p *auditLogPersister) List(page int, perPage int) ([]models.AuditLog, error) {
auditLogs := []models.AuditLog{}
err := p.db.Eager().Q().Paginate(page, perPage).Order("created_at desc").All(&auditLogs)
if err != nil && err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to fetch auditLogs: %w", err)
}
return auditLogs, nil
}
func (p *auditLogPersister) Delete(auditLog models.AuditLog) error {
err := p.db.Eager().Destroy(&auditLog)
if err != nil {
return fmt.Errorf("failed to delete auditlog: %w", err)
}
return nil
}

View File

@ -0,0 +1 @@
drop_table("audit_logs")

View File

@ -0,0 +1,14 @@
create_table("audit_logs") {
t.Column("id", "uuid", {"primary": true})
t.Column("type", "string", {})
t.Column("error", "string", {})
t.Column("meta_http_request_id", "string", {})
t.Column("meta_source_ip", "string", {})
t.Column("meta_user_agent", "string", {})
t.Column("actor_user_id", "uuid", {"null": true})
t.Column("actor_email", "string", {})
t.Timestamps()
t.Index("type")
t.Index("actor_user_id")
t.Index("actor_email")
}

View File

@ -0,0 +1,46 @@
package models
import (
"github.com/gofrs/uuid"
"time"
)
type AuditLog struct {
ID uuid.UUID `db:"id" json:"id"`
Type AuditLogType `db:"type" json:"type"`
Error string `db:"error" json:"error"`
MetaHttpRequestId string `db:"meta_http_request_id" json:"meta_http_request_id"`
MetaSourceIp string `db:"meta_source_ip" json:"meta_source_ip"`
MetaUserAgent string `db:"meta_user_agent" json:"meta_user_agent"`
ActorUserId *uuid.UUID `db:"actor_user_id" json:"actor_user_id"`
ActorEmail string `db:"actor_email" json:"actor_email"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
type AuditLogType string
var (
AuditLogUserCreated AuditLogType = "user_created"
AuditLogPasswordSetSucceeded AuditLogType = "password_set_succeeded"
AuditLogPasswordSetFailed AuditLogType = "password_set_failed"
AuditLogPasswordLoginSucceeded AuditLogType = "password_login_succeeded"
AuditLogPasswordLoginFailed AuditLogType = "password_login_failed"
AuditLogPasscodeLoginInitSucceeded AuditLogType = "passcode_login_init_succeeded"
AuditLogPasscodeLoginInitFailed AuditLogType = "passcode_login_init_failed"
AuditLogPasscodeLoginSucceeded AuditLogType = "passcode_login_succeeded"
AuditLogPasscodeLoginFailed AuditLogType = "passcode_login_failed"
AuditLogWebAuthnRegistrationInitSucceeded AuditLogType = "webauthn_registration_init_succeeded"
AuditLogWebAuthnRegistrationInitFailed AuditLogType = "webauthn_registration_init_failed"
AuditLogWebAuthnRegistrationSucceeded AuditLogType = "webauthn_registration_succeeded"
AuditLogWebAuthnRegistrationFailed AuditLogType = "webauthn_registration_failed"
AuditLogWebAuthnAuthenticationInitSucceeded AuditLogType = "webauthn_authentication_init_succeeded"
AuditLogWebAuthnAuthenticationInitFailed AuditLogType = "webauthn_authentication_init_failed"
AuditLogWebAuthnAuthenticationSucceeded AuditLogType = "webauthn_authentication_succeeded"
AuditLogWebAuthnAuthenticationFailed AuditLogType = "webauthn_authentication_failed"
)

View File

@ -29,6 +29,8 @@ type Persister interface {
GetWebauthnSessionDataPersisterWithConnection(tx *pop.Connection) WebauthnSessionDataPersister GetWebauthnSessionDataPersisterWithConnection(tx *pop.Connection) WebauthnSessionDataPersister
GetJwkPersister() JwkPersister GetJwkPersister() JwkPersister
GetJwkPersisterWithConnection(tx *pop.Connection) JwkPersister GetJwkPersisterWithConnection(tx *pop.Connection) JwkPersister
GetAuditLogPersister() AuditLogPersister
GetAuditLogPersisterWithConnection(tx *pop.Connection) AuditLogPersister
} }
type Migrator interface { type Migrator interface {
@ -145,6 +147,14 @@ func (p *persister) GetJwkPersisterWithConnection(tx *pop.Connection) JwkPersist
return NewJwkPersister(tx) return NewJwkPersister(tx)
} }
func (p *persister) GetAuditLogPersister() AuditLogPersister {
return NewAuditLogPersister(p.DB)
}
func (p *persister) GetAuditLogPersisterWithConnection(tx *pop.Connection) AuditLogPersister {
return NewAuditLogPersister(tx)
}
func (p *persister) Transaction(fn func(tx *pop.Connection) error) error { func (p *persister) Transaction(fn func(tx *pop.Connection) error) error {
return p.DB.Transaction(fn) return p.DB.Transaction(fn)
} }

View File

@ -31,5 +31,10 @@ func NewPrivateRouter(persister persistence.Persister) *echo.Echo {
user.PATCH("/:id", userHandler.Patch) user.PATCH("/:id", userHandler.Patch)
user.GET("", userHandler.List) user.GET("", userHandler.List)
auditLogHandler := handler.NewAuditLogHandler(persister)
auditLogs := e.Group("/audit_logs")
auditLogs.GET("", auditLogHandler.List)
return e return e
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config" "github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto/jwk" "github.com/teamhanko/hanko/backend/crypto/jwk"
"github.com/teamhanko/hanko/backend/dto" "github.com/teamhanko/hanko/backend/dto"
@ -49,15 +50,17 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister) *echo.
panic(fmt.Errorf("failed to create mailer: %w", err)) panic(fmt.Errorf("failed to create mailer: %w", err))
} }
auditLogClient := auditlog.NewClient(persister, cfg.AuditLog)
if cfg.Password.Enabled { if cfg.Password.Enabled {
passwordHandler := handler.NewPasswordHandler(persister, sessionManager, cfg) passwordHandler := handler.NewPasswordHandler(persister, sessionManager, cfg, auditLogClient)
password := e.Group("/password") password := e.Group("/password")
password.PUT("", passwordHandler.Set, hankoMiddleware.Session(sessionManager)) password.PUT("", passwordHandler.Set, hankoMiddleware.Session(sessionManager))
password.POST("/login", passwordHandler.Login) password.POST("/login", passwordHandler.Login)
} }
userHandler := handler.NewUserHandler(persister) userHandler := handler.NewUserHandler(persister, auditLogClient)
e.GET("/me", userHandler.Me, hankoMiddleware.Session(sessionManager)) e.GET("/me", userHandler.Me, hankoMiddleware.Session(sessionManager))
@ -68,11 +71,11 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister) *echo.
e.POST("/user", userHandler.GetUserIdByEmail) e.POST("/user", userHandler.GetUserIdByEmail)
healthHandler := handler.NewHealthHandler() healthHandler := handler.NewHealthHandler()
webauthnHandler, err := handler.NewWebauthnHandler(cfg, persister, sessionManager) webauthnHandler, err := handler.NewWebauthnHandler(cfg, persister, sessionManager, auditLogClient)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to create public webauthn handler: %w", err)) panic(fmt.Errorf("failed to create public webauthn handler: %w", err))
} }
passcodeHandler, err := handler.NewPasscodeHandler(cfg, persister, sessionManager, mailer) passcodeHandler, err := handler.NewPasscodeHandler(cfg, persister, sessionManager, mailer, auditLogClient)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to create public passcode handler: %w", err)) panic(fmt.Errorf("failed to create public passcode handler: %w", err))
} }

View File

@ -0,0 +1,18 @@
package test
import (
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/persistence/models"
)
func NewAuditLogClient() auditlog.Client {
return &auditLogClient{}
}
type auditLogClient struct {
}
func (a *auditLogClient) Create(context echo.Context, logType models.AuditLogType, user *models.User, err error) error {
return nil
}

View File

@ -0,0 +1,76 @@
package test
import (
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/persistence/models"
)
func NewAuditLogPersister(init []models.AuditLog) persistence.AuditLogPersister {
if init == nil {
return &auditLogPersister{[]models.AuditLog{}}
}
return &auditLogPersister{append([]models.AuditLog{}, init...)}
}
type auditLogPersister struct {
logs []models.AuditLog
}
func (p *auditLogPersister) Create(auditLog models.AuditLog) error {
p.logs = append(p.logs, auditLog)
return nil
}
func (p *auditLogPersister) Get(id uuid.UUID) (*models.AuditLog, error) {
var found *models.AuditLog
for _, data := range p.logs {
if data.ID == id {
d := data
found = &d
}
}
return found, nil
}
func (p *auditLogPersister) List(page int, perPage int) ([]models.AuditLog, error) {
if len(p.logs) == 0 {
return p.logs, nil
}
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 20
}
var result [][]models.AuditLog
var j int
for i := 0; i < len(p.logs); i += perPage {
j += perPage
if j > len(p.logs) {
j = len(p.logs)
}
result = append(result, p.logs[i:j])
}
if page > len(result) {
return []models.AuditLog{}, nil
}
return result[page-1], nil
}
func (p *auditLogPersister) Delete(auditLog models.AuditLog) error {
index := -1
for i, log := range p.logs {
if log.ID == auditLog.ID {
index = i
}
}
if index > -1 {
p.logs = append(p.logs[:index], p.logs[index+1:]...)
}
return nil
}

View File

@ -6,7 +6,7 @@ import (
"github.com/teamhanko/hanko/backend/persistence/models" "github.com/teamhanko/hanko/backend/persistence/models"
) )
func NewPersister(user []models.User, passcodes []models.Passcode, jwks []models.Jwk, credentials []models.WebauthnCredential, sessionData []models.WebauthnSessionData, passwords []models.PasswordCredential) persistence.Persister { func NewPersister(user []models.User, passcodes []models.Passcode, jwks []models.Jwk, credentials []models.WebauthnCredential, sessionData []models.WebauthnSessionData, passwords []models.PasswordCredential, auditLogs []models.AuditLog) persistence.Persister {
return &persister{ return &persister{
userPersister: NewUserPersister(user), userPersister: NewUserPersister(user),
passcodePersister: NewPasscodePersister(passcodes), passcodePersister: NewPasscodePersister(passcodes),
@ -14,6 +14,7 @@ func NewPersister(user []models.User, passcodes []models.Passcode, jwks []models
webauthnCredentialPersister: NewWebauthnCredentialPersister(credentials), webauthnCredentialPersister: NewWebauthnCredentialPersister(credentials),
webauthnSessionDataPersister: NewWebauthnSessionDataPersister(sessionData), webauthnSessionDataPersister: NewWebauthnSessionDataPersister(sessionData),
passwordCredentialPersister: NewPasswordCredentialPersister(passwords), passwordCredentialPersister: NewPasswordCredentialPersister(passwords),
auditLogPersister: NewAuditLogPersister(auditLogs),
} }
} }
@ -24,6 +25,7 @@ type persister struct {
webauthnCredentialPersister persistence.WebauthnCredentialPersister webauthnCredentialPersister persistence.WebauthnCredentialPersister
webauthnSessionDataPersister persistence.WebauthnSessionDataPersister webauthnSessionDataPersister persistence.WebauthnSessionDataPersister
passwordCredentialPersister persistence.PasswordCredentialPersister passwordCredentialPersister persistence.PasswordCredentialPersister
auditLogPersister persistence.AuditLogPersister
} }
func (p *persister) GetPasswordCredentialPersister() persistence.PasswordCredentialPersister { func (p *persister) GetPasswordCredentialPersister() persistence.PasswordCredentialPersister {
@ -81,3 +83,11 @@ func (p *persister) GetJwkPersister() persistence.JwkPersister {
func (p *persister) GetJwkPersisterWithConnection(tx *pop.Connection) persistence.JwkPersister { func (p *persister) GetJwkPersisterWithConnection(tx *pop.Connection) persistence.JwkPersister {
return p.jwkPersister return p.jwkPersister
} }
func (p *persister) GetAuditLogPersister() persistence.AuditLogPersister {
return p.auditLogPersister
}
func (p *persister) GetAuditLogPersisterWithConnection(tx *pop.Connection) persistence.AuditLogPersister {
return p.auditLogPersister
}