Skip to main content

Audit Logs

Query and display authentication audit history in your app. Audit logging is enabled by default --- this guide shows how to configure it, query events from the backend, and display login activity in your frontend.

Sample apps

The code in this guide is taken directly from the example apps. If you get stuck, clone and run them:

Prerequisites

  • A working auth setup (Quick Start)
  • Audit logging enabled (default --- no configuration needed)

Step 1: Configure

Audit logging is enabled by default. No configuration is required for most setups:

config/auth.config.ts
{
auditLogs: {
enabled: true, // default
},
}

For high-throughput environments, enable fire-and-forget mode to avoid awaiting audit writes on the request path:

config/auth.config.ts
{
auditLogs: {
enabled: true,
fireAndForget: true, // Don't await audit writes
},
}
OptionTypeDefaultDescription
enabledbooleantrueEnable/disable audit logging
fireAndForgetbooleanfalseDon't await audit writes on request path
Disabling Audit Logs

Disabling audit logs reduces security observability. Keep audit logging enabled in production systems.

Step 2: Query Audit History (Backend)

Inject AuthAuditService to query audit records from your backend code:

src/audit/audit.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { AuthAuditService } from '@nauth-toolkit/nestjs';
import { AuthAuditEventType } from '@nauth-toolkit/core';

@Controller('admin/audit')
export class AuditController {
constructor(private readonly auditService: AuthAuditService) {}

@Get('user-history')
async getUserHistory(
@Query('sub') sub: string,
@Query('page') page = 1,
@Query('limit') limit = 50,
) {
return this.auditService.getUserAuthHistory({
sub,
page,
limit,
});
}

@Get('suspicious')
async getSuspicious() {
return this.auditService.getSuspiciousActivity({
limit: 100,
});
}

@Get('by-type')
async getByType(
@Query('eventType') eventType: AuthAuditEventType,
@Query('page') page = 1,
) {
return this.auditService.getEventsByType({
eventType,
page,
limit: 50,
});
}
}

Query Methods

MethodPurposeParameters
getUserAuthHistory()Full history for one usersub, page, limit, eventTypes[], eventStatus[], startDate, endDate
getEventsByType()All events of a specific typeeventType, page, limit, startDate, endDate
getSuspiciousActivity()Suspicious/risky eventssub? (optional), limit
getRiskAssessmentHistory()Adaptive MFA risk eventssub, limit

Filtering Examples

// Login failures in the last 24 hours
const failures = await auditService.getUserAuthHistory({
sub: 'user-uuid',
eventTypes: [AuthAuditEventType.LOGIN_FAILED, AuthAuditEventType.LOGIN_BLOCKED],
eventStatus: ['FAILURE'],
startDate: new Date(Date.now() - 24 * 60 * 60 * 1000),
page: 1,
limit: 50,
});

// All suspicious activity across all users
const suspicious = await auditService.getSuspiciousActivity({
limit: 100,
});

// All social logins this month
const socialLogins = await auditService.getEventsByType({
eventType: AuthAuditEventType.SOCIAL_LOGIN,
startDate: new Date('2025-02-01'),
page: 1,
limit: 100,
});

Response Format

All query methods return paginated responses:

{
"data": [
{
"id": 1042,
"userId": 15,
"eventType": "LOGIN_SUCCESS",
"eventStatus": "SUCCESS",
"ipAddress": "203.0.113.42",
"ipCountry": "US",
"ipCity": "San Francisco",
"platform": "macOS",
"browser": "Chrome",
"deviceName": "Chrome on MacBook",
"deviceType": "desktop",
"authMethod": "password",
"performedBy": "550e8400-e29b-41d4-a716-446655440000",
"metadata": null,
"createdAt": "2025-02-22T14:30:00.000Z"
},
{
"id": 1041,
"userId": 15,
"eventType": "MFA_VERIFICATION_SUCCESS",
"eventStatus": "SUCCESS",
"ipAddress": "203.0.113.42",
"ipCountry": "US",
"ipCity": "San Francisco",
"platform": "macOS",
"browser": "Chrome",
"authMethod": "totp",
"metadata": {
"deviceType": "totp",
"deviceName": "Authenticator App"
},
"createdAt": "2025-02-22T14:29:55.000Z"
},
{
"id": 1038,
"userId": 15,
"eventType": "LOGIN_FAILED",
"eventStatus": "FAILURE",
"ipAddress": "198.51.100.23",
"ipCountry": "DE",
"ipCity": "Berlin",
"platform": "Windows",
"browser": "Firefox",
"deviceType": "desktop",
"authMethod": "password",
"reason": "Invalid credentials",
"metadata": null,
"createdAt": "2025-02-22T10:15:00.000Z"
}
],
"total": 247,
"page": 1,
"limit": 50,
"totalPages": 5
}

Step 3: Display Audit History (Frontend)

Current User's History

Use the client SDK to fetch the authenticated user's own audit history:

src/app/security/audit-history.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '@nauth-toolkit/client-angular';
import { AuditHistoryResponse, AuthAuditEvent } from '@nauth-toolkit/client';

@Component({
selector: 'app-audit-history',
template: `
<h2>Login Activity</h2>
<table>
<thead>
<tr>
<th>Event</th>
<th>Status</th>
<th>Location</th>
<th>Device</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@for (event of events; track event.id) {
<tr [class.suspicious]="event.eventStatus === 'SUSPICIOUS'">
<td>{{ formatEventType(event.eventType) }}</td>
<td>{{ event.eventStatus }}</td>
<td>{{ event.ipCity }}, {{ event.ipCountry }}</td>
<td>{{ event.deviceName || event.browser }}</td>
<td>{{ event.createdAt | date:'medium' }}</td>
</tr>
}
</tbody>
</table>
`,
})
export class AuditHistoryComponent implements OnInit {
events: AuthAuditEvent[] = [];

constructor(private auth: AuthService) {}

async ngOnInit() {
const response = await this.auth.getAuditHistory({
page: 1,
limit: 20,
});
this.events = response.data;
}

formatEventType(type: string): string {
return type.replace(/_/g, ' ').toLowerCase()
.replace(/^\w/, c => c.toUpperCase());
}
}

Admin: Any User's History

Use the admin operations client to query any user's audit history:

const history = await this.auth.admin?.getAuditHistory({
sub: 'user-uuid',
page: 1,
limit: 50,
eventType: 'LOGIN_FAILED',
});

Troubleshooting

No audit records being created:

  1. Verify auditLogs.enabled is not set to false
  2. Check server logs for audit recording errors
  3. Verify the database migration has run (audit table exists)

Missing geolocation data:

  1. Configure Geolocation --- ipCountry, ipCity, and coordinates require MaxMind
  2. Without geolocation, only ipAddress is captured

Audit endpoint returns 404:

  1. Verify audit logging is enabled in your config
  2. Check that your backend exposes the audit endpoints (NestJS auto-registers them)

High database growth:

  1. Enable fireAndForget: true for write performance
  2. Implement retention policies --- periodically delete old records
  3. Consider archiving to cold storage for compliance

What's Next

  • Audit Logs --- How audit logging works (event types catalog, data model, metadata examples)
  • Lifecycle Hooks --- React to auth events with custom logic
  • Geolocation --- Enable location data in audit records
  • Admin Operations --- User management and session control