mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-28 23:30:15 +08:00
feat: add audit logs
This commit is contained in:
@ -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
|
||||||
|
|||||||
70
backend/audit_log/client.go
Normal file
70
backend/audit_log/client.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
41
backend/handler/audit_log.go
Normal file
41
backend/handler/audit_log.go
Normal 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)
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)) {
|
||||||
|
|||||||
72
backend/persistence/audit_log_persister.go
Normal file
72
backend/persistence/audit_log_persister.go
Normal 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
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
drop_table("audit_logs")
|
||||||
@ -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")
|
||||||
|
}
|
||||||
46
backend/persistence/models/audit_log.go
Normal file
46
backend/persistence/models/audit_log.go
Normal 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"
|
||||||
|
)
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
18
backend/test/audit_log_client.go
Normal file
18
backend/test/audit_log_client.go
Normal 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
|
||||||
|
}
|
||||||
76
backend/test/audit_log_persister.go
Normal file
76
backend/test/audit_log_persister.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user