mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-15 00:35:14 +00:00
feat: adding/removing passkeys creates an entry in audit logs (#1099)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
committed by
GitHub
parent
a54b867105
commit
c56afe016e
@@ -57,7 +57,7 @@ func (wc *WebauthnController) verifyRegistrationHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
credential, err := wc.webAuthnService.VerifyRegistration(c.Request.Context(), sessionID, userID, c.Request)
|
credential, err := wc.webAuthnService.VerifyRegistration(c.Request.Context(), sessionID, userID, c.Request, c.ClientIP())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
@@ -134,8 +134,10 @@ func (wc *WebauthnController) listCredentialsHandler(c *gin.Context) {
|
|||||||
func (wc *WebauthnController) deleteCredentialHandler(c *gin.Context) {
|
func (wc *WebauthnController) deleteCredentialHandler(c *gin.Context) {
|
||||||
userID := c.GetString("userID")
|
userID := c.GetString("userID")
|
||||||
credentialID := c.Param("id")
|
credentialID := c.Param("id")
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
userAgent := c.Request.UserAgent()
|
||||||
|
|
||||||
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID)
|
err := wc.webAuthnService.DeleteCredential(c.Request.Context(), userID, credentialID, clientIP, userAgent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ const (
|
|||||||
AuditLogEventNewClientAuthorization AuditLogEvent = "NEW_CLIENT_AUTHORIZATION"
|
AuditLogEventNewClientAuthorization AuditLogEvent = "NEW_CLIENT_AUTHORIZATION"
|
||||||
AuditLogEventDeviceCodeAuthorization AuditLogEvent = "DEVICE_CODE_AUTHORIZATION"
|
AuditLogEventDeviceCodeAuthorization AuditLogEvent = "DEVICE_CODE_AUTHORIZATION"
|
||||||
AuditLogEventNewDeviceCodeAuthorization AuditLogEvent = "NEW_DEVICE_CODE_AUTHORIZATION"
|
AuditLogEventNewDeviceCodeAuthorization AuditLogEvent = "NEW_DEVICE_CODE_AUTHORIZATION"
|
||||||
|
AuditLogEventPasskeyAdded AuditLogEvent = "PASSKEY_ADDED"
|
||||||
|
AuditLogEventPasskeyRemoved AuditLogEvent = "PASSKEY_REMOVED"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scan and Value methods for GORM to handle the custom type
|
// Scan and Value methods for GORM to handle the custom type
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (s *AuditLogService) Create(ctx context.Context, event model.AuditLogEvent,
|
|||||||
country, city, err := s.geoliteService.GetLocationByIP(ipAddress)
|
country, city, err := s.geoliteService.GetLocationByIP(ipAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log the error but don't interrupt the operation
|
// Log the error but don't interrupt the operation
|
||||||
slog.Warn("Failed to get IP location", "error", err)
|
slog.Warn("Failed to get IP location", slog.String("ip", ipAddress), slog.Any("error", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
auditLog := model.AuditLog{
|
auditLog := model.AuditLog{
|
||||||
@@ -201,8 +201,8 @@ func (s *AuditLogService) ListUsernamesWithIds(ctx context.Context) (users map[s
|
|||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Joins("User").
|
Joins("User").
|
||||||
Model(&model.AuditLog{}).
|
Model(&model.AuditLog{}).
|
||||||
Select("DISTINCT \"User\".id, \"User\".username").
|
Select(`DISTINCT "User".id, "User".username`).
|
||||||
Where("\"User\".username IS NOT NULL")
|
Where(`"User".username IS NOT NULL`)
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
ID string `gorm:"column:id"`
|
ID string `gorm:"column:id"`
|
||||||
@@ -210,7 +210,8 @@ func (s *AuditLogService) ListUsernamesWithIds(ctx context.Context) (users map[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
var results []Result
|
var results []Result
|
||||||
if err := query.Find(&results).Error; err != nil {
|
err = query.Find(&results).Error
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query user IDs: %w", err)
|
return nil, fmt.Errorf("failed to query user IDs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +247,8 @@ func (s *AuditLogService) ListClientNames(ctx context.Context) (clientNames []st
|
|||||||
}
|
}
|
||||||
|
|
||||||
var results []Result
|
var results []Result
|
||||||
if err := query.Find(&results).Error; err != nil {
|
err = query.Find(&results).Error
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query client IDs: %w", err)
|
return nil, fmt.Errorf("failed to query client IDs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -114,7 +116,7 @@ func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID string)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, userID string, r *http.Request) (model.WebauthnCredential, error) {
|
func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID string, userID string, r *http.Request, ipAddress string) (model.WebauthnCredential, error) {
|
||||||
tx := s.db.Begin()
|
tx := s.db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -173,6 +175,9 @@ func (s *WebAuthnService) VerifyRegistration(ctx context.Context, sessionID, use
|
|||||||
return model.WebauthnCredential{}, fmt.Errorf("failed to store WebAuthn credential: %w", err)
|
return model.WebauthnCredential{}, fmt.Errorf("failed to store WebAuthn credential: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auditLogData := model.AuditLogData{"credentialID": hex.EncodeToString(credential.ID), "passkeyName": passkeyName}
|
||||||
|
s.auditLogService.Create(ctx, model.AuditLogEventPasskeyAdded, ipAddress, r.UserAgent(), userID, auditLogData, tx)
|
||||||
|
|
||||||
err = tx.Commit().Error
|
err = tx.Commit().Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.WebauthnCredential{}, fmt.Errorf("failed to commit transaction: %w", err)
|
return model.WebauthnCredential{}, fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
@@ -288,16 +293,30 @@ func (s *WebAuthnService) ListCredentials(ctx context.Context, userID string) ([
|
|||||||
return credentials, nil
|
return credentials, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID, credentialID string) error {
|
func (s *WebAuthnService) DeleteCredential(ctx context.Context, userID string, credentialID string, ipAddress string, userAgent string) error {
|
||||||
err := s.db.
|
tx := s.db.Begin()
|
||||||
|
defer func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
credential := &model.WebauthnCredential{}
|
||||||
|
err := tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Where("id = ? AND user_id = ?", credentialID, userID).
|
Clauses(clause.Returning{}).
|
||||||
Delete(&model.WebauthnCredential{}).
|
Delete(credential, "id = ? AND user_id = ?", credentialID, userID).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete record: %w", err)
|
return fmt.Errorf("failed to delete record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auditLogData := model.AuditLogData{"credentialID": hex.EncodeToString(credential.CredentialID), "passkeyName": credential.Name}
|
||||||
|
s.auditLogService.Create(ctx, model.AuditLogEventPasskeyRemoved, ipAddress, userAgent, userID, auditLogData, tx)
|
||||||
|
|
||||||
|
err = tx.Commit().Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +372,7 @@ func (s *WebAuthnService) CreateReauthenticationTokenWithAccessToken(ctx context
|
|||||||
|
|
||||||
userID, ok := token.Subject()
|
userID, ok := token.Subject()
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("access token does not contain user ID")
|
return "", errors.New("access token does not contain user ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is issued less than a minute ago
|
// Check if token is issued less than a minute ago
|
||||||
|
|||||||
@@ -331,6 +331,10 @@
|
|||||||
"token_sign_in": "Token Sign In",
|
"token_sign_in": "Token Sign In",
|
||||||
"client_authorization": "Client Authorization",
|
"client_authorization": "Client Authorization",
|
||||||
"new_client_authorization": "New Client Authorization",
|
"new_client_authorization": "New Client Authorization",
|
||||||
|
"device_code_authorization": "Device Code Authorization",
|
||||||
|
"new_device_code_authorization": "New Device Code Authorization",
|
||||||
|
"passkey_added": "Passkey Added",
|
||||||
|
"passkey_removed": "Passkey Removed",
|
||||||
"disable_animations": "Disable Animations",
|
"disable_animations": "Disable Animations",
|
||||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||||
"user_disabled": "Account Disabled",
|
"user_disabled": "Account Disabled",
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ export const eventTypes: Record<string, string> = {
|
|||||||
TOKEN_SIGN_IN: m.token_sign_in(),
|
TOKEN_SIGN_IN: m.token_sign_in(),
|
||||||
CLIENT_AUTHORIZATION: m.client_authorization(),
|
CLIENT_AUTHORIZATION: m.client_authorization(),
|
||||||
NEW_CLIENT_AUTHORIZATION: m.new_client_authorization(),
|
NEW_CLIENT_AUTHORIZATION: m.new_client_authorization(),
|
||||||
ACCOUNT_CREATED: m.account_created()
|
ACCOUNT_CREATED: m.account_created(),
|
||||||
|
DEVICE_CODE_AUTHORIZATION: m.device_code_authorization(),
|
||||||
|
NEW_DEVICE_CODE_AUTHORIZATION: m.new_device_code_authorization(),
|
||||||
|
PASSKEY_ADDED: m.passkey_added(),
|
||||||
|
PASSKEY_REMOVED: m.passkey_removed(),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user