Integration guide
Three server-to-server calls on your auth path, and an optional browser client for the token gate. Everything below is fail-open: if ATP is unreachable, your logins carry on exactly as they do today.
Add a site and collect your credentials
In your srvAudit portal, open Threat Protection and add the site you want protected. You'll be given a site ID and a server key. The server key is shown once and stored only as a digest, so keep it somewhere safe — a lost key is rotated, never recovered.
ATP_URL=https://atp.srvaudit.com
ATP_SITE_ID=site_01jq8xkz3n7v9wq2m4h6bcdefg
ATP_SERVER_KEY=satp_sk_...
// config/services.php
'atp' => [
'url' => env('ATP_URL', 'https://atp.srvaudit.com'),
'site' => env('ATP_SITE_ID'),
'server_key' => env('ATP_SERVER_KEY'),
],
Install the package
Laravel applications get a first-party package. Without a site id and server key it is completely inert, so it is safe to install before you have credentials.
composer require srvaudit/atp
Every call that touches the network is wrapped so a timeout, a refused connection or a 500 resolves to allow, on a 50ms connect and 150ms read budget. That is what guarantees an ATP problem can never become an authentication problem — please don't tune it away.
Not on Laravel? Everything below is plain JSON over HTTPS. Implement the same fail-open wrapper in your own stack and the rest works identically.
Put it on your login route
The middleware runs the pre-auth decision, and Laravel's own authentication events report the outcome for you. For a standard application that is the entire integration.
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->middleware('atp.protect');
A blocked attempt comes back looking exactly like a wrong password. Telling an attacker they were blocked tells them what to change.
Using two-factor auth?
The listener cannot tell whether a completed login is the end of the story or the first of two factors. Turn it off and report mfa_pending yourself — a pending challenge is not a failed credential, and counting it as one penalises your real users.
Prefer to wire it by hand? verify before you authenticate, report after. The report attributes the outcome to the account that was actually tried, which is what turns a raw failure count into per-account accounting.
public function store(LoginRequest $request, Atp $atp)
{
$context = $atp->contextFrom($request);
if (! $atp->allows($context)) {
return back()->withErrors(['email' => __('auth.failed')]);
}
if (! Auth::attempt($request->credentials())) {
$atp->report($context, 'failure');
return back()->withErrors(['email' => __('auth.failed')]);
}
$atp->report($context, 'success');
return redirect()->intended();
}
Report accurately
Use mfa_pending when credentials were correct but a second factor is outstanding, and locked when the account is locked out. A pending MFA challenge is not a failed credential and is deliberately not counted against any failure budget.
Mint tokens in the browser
The token gate is the structural fix for a distributed attack, because address rotation says nothing about whether the handshake was completed. Mint the token while the user fills the form so the solve never sits on the critical path.
import { AtpClient } from './atp-client.js';
const atp = new AtpClient({
baseUrl: 'https://atp.srvaudit.com',
siteId: 'site_01jq8xkz3n7v9wq2m4h6bcdefg',
});
// Warm the token as soon as the form is shown.
document.querySelector('#login-form')
.addEventListener('focusin', () => atp.getToken(), { once: true });
form.addEventListener('submit', async (event) => {
event.preventDefault();
await fetch('/login', {
method: 'POST',
headers: { ...await atp.headers(), 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
});
Instrument your startup sequence
Real clients do things before they reach a login: fetch config, resolve a server, load strings. Attack traffic jumps straight to the login. Marking those steps builds the provenance the boot-trail signal reads.
Route::get('/api/config', function (Request $request) {
Atp::observe($request->header('x-device-id'), 'app_config');
return AppConfig::current();
});
Roll this out in order
Instrument first, so real sessions start leaving breadcrumbs. Watch the signal while observing until it reliably fires on attack traffic and not on real logins. Only then let it contribute to a block. Enforcing before your clients are instrumented would fire the signal on everyone — which is why the signal stays silent until you configure required steps.
Reject breached passwords
We mirror the Have I Been Pwned corpus and refresh it monthly, so a password check never becomes a request to a third party on your login path.
The lookup is k-anonymous: the package hashes the password locally and sends only the first five characters of that digest. We return every suffix under that prefix and your application does the comparison. The password never leaves your process, and we cannot tell which entry you were asking about.
use SrvAudit\Atp\Rules\NotBreached;
$request->validate([
'password' => ['required', Password::defaults(), new NotBreached],
]);
Registration, not login
Use this when a password is being set or changed. Rejecting an existing user's password at sign-in locks them out of their own account with no way back in. A lookup that cannot be performed passes, because a breach service being down is not a reason to stop somebody setting a password.
The middleware also forwards a boolean to verify, where a breached password becomes one behavioural signal among several. It is weighted below the block threshold on purpose: plenty of real people reuse a breached password, and on its own that is a reason to prompt a reset, never to reject a login.
Watch, then enforce
Your site starts in observing mode: the verdict is always allow, and the would-have-blocked volume is recorded. The portal shows a readiness checklist covering token adoption, error counts, and whether your startup instrumentation is actually reporting. When it's green, switch to enforcing — and switch back with the same control if anything looks wrong.
Endpoint reference
| Endpoint | Auth | Purpose |
|---|---|---|
| GET /atp/{site}/challenge | Public | Issue a signed proof-of-work challenge |
| POST /atp/{site}/token | Public | Redeem a solved challenge for a token |
| POST /atp/{site}/verify | Server key | Pre-auth allow or block decision |
| POST /atp/{site}/report | Server key | Post-auth outcome; updates the counters |
| POST /atp/{site}/observe | Server key | Record a pre-login ceremony step |
| GET /atp/{site}/pwned/{prefix} | Server key | Breached-password suffixes for five hex characters of a SHA-1 |
| GET /atp/{site}/health | Server key | Current mode and enforcement state |
| GET /atp/health | Public | Service liveness |
The public and server-to-server routes carry separate throttle budgets, so a flood against challenge and token can never starve the verify and report calls on your login path.
What we store
- Account identifiers are never stored. They are reduced to a keyed HMAC, salted per site, before anything is written down. You can send the digest yourself if you would rather the identifier never left your network.
- Counters carry an expiry and are read as absent once lapsed, so a window stops counting without needing to be swept.
- Telemetry is hourly rollups, not a per-request log, and is pruned on your plan's retention window.
- Passwords are never sent, never accepted, and have no field in any request.