1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-15 20:25:05 +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:
Alessandro (Ale) Segala
2025-11-16 14:51:38 -08:00
committed by GitHub
parent a54b867105
commit c56afe016e
6 changed files with 47 additions and 14 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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",

View File

@@ -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(),
} }
/** /**