diff --git a/authentication/serializers.py b/authentication/serializers.py
index f90a5cd42e7b0b878919ad07735084a3e1464bba..2190aebd1111dbef8f78115be49bba0fe68bf164 100644
--- a/authentication/serializers.py
+++ b/authentication/serializers.py
@@ -100,6 +100,7 @@ class TokenRequestSerializer(serializers.Serializer):
                 identity_key=identity_key,
                 date_of_birth=date_of_birth,
                 lockout_until=lockout.lockout_until,
+                category="audit",
             )
             raise InvalidGrantError("Account locked out")
 
@@ -129,6 +130,7 @@ class TokenRequestSerializer(serializers.Serializer):
                         last_name=data.get("last_name"),
                         date_of_birth=data["date_of_birth"],
                         name_match=name_match,
+                        category="audit",
                     )
                     raise InvalidGrantError("Ambiguous token request")
                 except Account.DoesNotExist:
@@ -159,6 +161,7 @@ class TokenRequestSerializer(serializers.Serializer):
                 last_name=data.get("last_name"),
                 date_of_birth=data.get("date_of_birth"),
                 name_match=name_match,
+                category="audit",
             )
             raise InvalidGrantError("No matching account")
 
@@ -168,6 +171,16 @@ class TokenRequestSerializer(serializers.Serializer):
         # Set account in validated data
         data["account"] = account
 
+        logger.info(
+            "Successful authentication",
+            crsid=data.get("crsid"),
+            last_name=data.get("last_name"),
+            date_of_birth=data.get("date_of_birth"),
+            name_match=name_match,
+            account=account.crsid,
+            category="audit",
+        )
+
         return data
 
 
diff --git a/authentication/tests/test_lockout.py b/authentication/tests/test_lockout.py
index ad9eb59204d04b7bb7cc85df394fc3d23735639e..2ef60c607f38f79d00bebbf6f34e12f7408143b3 100644
--- a/authentication/tests/test_lockout.py
+++ b/authentication/tests/test_lockout.py
@@ -135,7 +135,7 @@ def test_exponential_backoff_lockout(identity_field):
 
 @pytest.mark.parametrize("identity_field", ["crsid", "last_name"])
 @freeze_time("2025-01-01 00:00:00")
-def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client):
+def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client, caplog):
     account = AccountFactory(account_identifier=True)
     identifier = account.account_identifier.first()
     identity_value = account.crsid if identity_field == "crsid" else identifier.last_name
@@ -164,11 +164,15 @@ def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client):
     # Confirm account is locked out
     assert Lockout.objects.filter(**lockout_kwargs).exists()
 
+    caplog.clear()
     response = api_client.post(
         reverse("knox_login"),
         data=urlencode(data),
         content_type="application/x-www-form-urlencoded",
     )
+    # Confirm that the audit log entry was made
+    assert "Account locked out" in caplog.text
+    assert "'category': 'audit'" in caplog.text
 
     assert response.status_code == 400
 
diff --git a/authentication/tests/test_login_view.py b/authentication/tests/test_login_view.py
index d953bc68ad24923b1b5d369f9aef22f2923618bb..55e533333716cc5e69358a6bef082ae11d03e47f 100644
--- a/authentication/tests/test_login_view.py
+++ b/authentication/tests/test_login_view.py
@@ -83,9 +83,12 @@ def test_unsupported_media_type_multipart(client, valid_session_grant_body, pars
 
 
 @pytest.mark.parametrize("valid_session_grant_body", ["crsid", "last_name"], indirect=True)
-def test_valid_session_grant(client, valid_session_grant_body):
+def test_valid_session_grant(client, valid_session_grant_body, caplog):
     """A valid session grant request body succeeds"""
     assert_is_valid_login(post_login(client, valid_session_grant_body))
+    # Confirm that the audit log entry was made
+    assert "Successful authentication" in caplog.text
+    assert "'category': 'audit'" in caplog.text
 
 
 @pytest.mark.parametrize("valid_session_grant_body", ["crsid"], indirect=True)
@@ -176,7 +179,7 @@ def test_missing_field(client, request, valid_session_grant_body):
     assert_is_error(post_login(client, missing_field_body), "invalid_request")
 
 
-def test_invalid_grant(faker, client):
+def test_invalid_grant(faker, client, caplog):
     """Providing credentials that do not exist"""
     assert_is_error(
         post_login(
@@ -190,6 +193,9 @@ def test_invalid_grant(faker, client):
         ),
         "invalid_grant",
     )
+    # Confirm that the audit log entry was made
+    assert "No matching user" in caplog.text
+    assert "'category': 'audit'" in caplog.text
 
 
 @pytest.mark.parametrize("valid_session_grant_body", ["crsid", "last_name"], indirect=True)
diff --git a/authentication/views.py b/authentication/views.py
index 61cc1aa61a3cce1af7e6385ce731adcc176e3f11..d05fd87b79aa751ca8292dfe12cdd8b36e4b26e2 100644
--- a/authentication/views.py
+++ b/authentication/views.py
@@ -1,3 +1,4 @@
+import structlog
 from drf_spectacular.utils import OpenApiResponse, extend_schema
 from knox.views import LoginView as KnoxLoginView
 from rest_framework import (
@@ -21,6 +22,8 @@ from authentication.serializers import (
     TokenResponseSerializer,
 )
 
+logger = structlog.get_logger(__name__)
+
 
 class LoginView(KnoxLoginView):
     throttle_classes = ()
@@ -177,3 +180,14 @@ class LockoutViewSet(
     queryset = Lockout.objects.all()
     serializer_class = LockoutSerializer
     versioning_class = None
+
+    def destroy(self, request, *args, **kwargs):
+        instance = self.get_object()
+        logger.info(
+            "Lockout removed",
+            identity_key=instance.identity_key,
+            date_of_birth=instance.date_of_birth,
+            category="audit",
+        )
+        self.perform_destroy(instance)
+        return Response(status=status.HTTP_204_NO_CONTENT)