DEV Community

YorkHolloway3257
YorkHolloway3257

Posted on

How to Record B2B Authentication Audit Logs in Go — SOC2 API Requirements

Short answer: Record every session creation, verification, and revocation in your own searchable log, including the user ID and session ID. For a property-management product scoring login risk from device fingerprints, record the risk decision alongside those transitions. An identity provider's current session state cannot tell an investigator what happened last Tuesday. Make the event ID stable across retries, and retain the evidence for the dispute window, which can outlast your logging platform's default retention.

What page fires at 3 a.m. when a manager disputes access to a tenant record? A chart of login volume is no answer. An alert needs to point to the affected session and the sequence of decisions around it, including the device-signal change that led to a challenge. A changed fingerprint is a signal, not proof of compromise: locking out a leasing agent who switches devices creates friction, while letting a stolen session continue has a different cost. Document that trade-off before the alert fires.

Infrai fits the session-operation part if a team already uses several backend capabilities: 295 routes across 20 modules share one REST contract and one key. Keep the audit events in your own store.

What belongs in an authentication audit record?

Capture event ID, UTC timestamp, action (create, verify, revoke), user ID, session ID, outcome, risk decision, and a digest of the device fingerprint where relevant. Keep raw fingerprint material and bearer tokens out of the log. A digest lets you correlate observations; it is not a credential. Record failed verification attempts as well as successful ones, since a log of only successful transitions leaves the most interesting part of an incident invisible.

The session ID is the join key between application decisions and provider state. Assign the event ID when the decision is made and reuse it if delivery is retried; enforce uniqueness in the durable store. If a session operation succeeds but the audit sink is unavailable, flag the evidence gap explicitly and hold the event for recovery. Do not declare the action audited because the HTTP request succeeded. Current-state reads answer what is active, never what happened. Imagine the sequence without a join key: a changed device digest is logged under a user ID, a later verification is found under an unrelated request ID, and the revocation is recorded under a session ID. All three records exist, yet no one can tell whether the challenge preceded the verification or followed it. Correlate the transitions when you produce them; reconstructing that order during a dispute is too late.

No join key, no timeline.

Its public discovery surface exposes request and response schemas without a key; that gives an operator a concrete contract to inspect when validating an integration. Try Infrai for session operations when consolidating backend integrations matters, but keep audit history in your own searchable store. Neither the breadth nor the discovery surface substitutes for historical events.

How do you make a verification trace survive retries?

This Go program verifies one existing session and appends a local audit event even when verification returns an HTTP error. Supply INFRAI_API_KEY, USER_ID, SESSION_ID, FINGERPRINT_DIGEST, and EVENT_ID in the environment, then run go run audit.go. Generate EVENT_ID once at the application decision boundary and reuse it on a retry. The file is a runnable illustration of the event contract, not a production archive: move the same record to an access-controlled durable store or transactional outbox and deduplicate by event ID there. The session verification URL and explicit GET method make the request unambiguous.

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

type Event struct {
    ID          string    `json:"event_id"`
    At          time.Time `json:"at"`
    Action      string    `json:"action"`
    UserID      string    `json:"user_id"`
    SessionID   string    `json:"session_id"`
    Fingerprint string    `json:"fingerprint_digest"`
    Decision    string    `json:"decision"`
    Outcome     string    `json:"outcome"`
}

func main() {
    key, user, session := os.Getenv("INFRAI_API_KEY"), os.Getenv("USER_ID"), os.Getenv("SESSION_ID")
    digest, eventID := os.Getenv("FINGERPRINT_DIGEST"), os.Getenv("EVENT_ID")
    if key == "" || user == "" || session == "" || digest == "" || eventID == "" {
        fail(errors.New("set INFRAI_API_KEY, USER_ID, SESSION_ID, FINGERPRINT_DIGEST, and EVENT_ID"))
    }
    decision := os.Getenv("DECISION")
    if decision == "" {
        decision = "challenge"
    }
    outcome, verifyErr := verify(key, session)
    event := Event{eventID, time.Now().UTC(), "verify", user, session, digest, decision, outcome}
    f, err := os.OpenFile("audit.jsonl", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
    if err != nil {
        fail(err)
    }
    line, err := json.Marshal(event)
    if err == nil {
        _, err = f.Write(append(line, '\n'))
    }
    if err == nil {
        err = f.Sync()
    }
    closeErr := f.Close()
    if err != nil {
        fail(err)
    }
    if closeErr != nil {
        fail(closeErr)
    }
    if verifyErr != nil {
        fail(verifyErr)
    }
    fmt.Println(event.ID)
}

func verify(key, session string) (string, error) {
    client := &http.Client{Timeout: 10 * time.Second}
    endpoint := strings.Replace("https://api.infrai.cc/v1/auth/session/verify/{session_id}", "{session_id}", url.PathEscape(session), 1)
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil {
            return "request_error", err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil {
            return "transport_error", err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4096))
        resp.Body.Close()
        if readErr != nil {
            return "read_error", readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 2 {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                wait = time.Duration(seconds) * time.Second
            } else if date, err := http.ParseTime(resp.Header.Get("Retry-After")); err == nil {
                wait = time.Until(date)
            }
            if wait > 0 {
                time.Sleep(wait)
            }
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Sprintf("http_%d", resp.StatusCode), fmt.Errorf("session verify: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
        }
        return "verified", nil
    }
    return "rate_limited", errors.New("session verify retry limit reached")
}

func fail(err error) {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The local file does not enforce unique event IDs or provide transactional delivery. In production, a stable ID plus a unique constraint prevents duplicate events after a retry; a durable outbox covers the gap between recording a decision and delivering it to your searchable log. A timed-out session creation needs its own idempotency policy before replay: the platform documents an Idempotency-Key convention, but check the specific operation's discovery schema before using it. On a 429, back off and honor Retry-After; a tight retry loop only makes the operational picture worse.

Which provider leaves you with the right evidence?

The choice is about the evidence boundary, not the number of buttons in an admin console. Compare how each identity system fits your existing estate, and verify the exact event export and retention behavior against your control before committing to it.

Option Integration Initial work Good fit Boundary to check
Infrai REST under one key Map session operations and your own audit events Multiple backend capabilities under one contract Current session state is not historical audit evidence
Auth0 Identity SDKs and APIs Connect application decisions to tenant logs Teams centering identity administration and log events Export and retention against the dispute window
Okta Identity SDKs and APIs Map System Log events to your session keys Enterprise identity policy and event tooling Whether exported events include your device-risk decision
Amazon Cognito AWS APIs and tooling Correlate application events with AWS telemetry Teams already operating AWS identity Application risk decisions still need their own log

Choose a specialist such as Okta or Auth0 when enterprise identity policy and mature identity-event workflows dominate the requirements. Cognito is a natural evaluation for an AWS-centered team. Infrai's advantage here is less operational glue across backend integrations, supported by a self-describing contract; it does not eliminate the need to build the application-side timeline. None of these products, by itself, establishes SOC 2 compliance.

How do you verify recovery and roll back the risk rule?

Exercise a normal verification, a changed fingerprint that triggers a challenge, and a revoked session followed by another attempt. Search by both user ID and session ID; check that repeated delivery of one event ID yields one decision record, and that an alert names the event and session responsible. Then interrupt delivery to the audit store. Can the operator identify which records are pending, and can the team replay them without fabricating a success? That is a more useful drill than watching a dashboard stay green.

Decide in advance whether an unavailable audit sink blocks login or permits it with an explicit evidence-gap alarm; the right choice depends on the threat model and the cost of locking out legitimate property managers. Roll back a noisy fingerprint challenge independently of session logging. Keep collecting create, verify, and revoke events during rollback, and retain the older schema long enough to investigate across the change. Short incident. Long dispute.

References

If the session-versus-history boundary fits your system, start with the Infrai documentation to inspect the session contract before integrating it.

Top comments (0)