← Blog

Passing Google's CASA Tier 2 Audit for Gmail OAuth

The real checklist, actual costs, and every finding from the OWASP ZAP scan.

What is CASA Tier 2?

If your app uses Gmail's restricted OAuth scopes (gmail.readonly, gmail.send, gmail.modify), Google requires a Cloud Application Security Assessment (CASA) before your app can go to production. Without it, your OAuth consent screen stays in "Testing" mode — capped at 100 users, with a scary warning screen.

CASA has multiple tiers. Tier 2 is the one that applies to most apps using restricted scopes. It requires:

  1. An OWASP ZAP DAST scan of your entire application
  2. Assessment by an authorized lab (from the App Defense Alliance list)
  3. Remediation of all findings
  4. A Letter of Validation (LOV) submitted to Google
  5. Annual recertification

This post covers what we actually encountered when inbox.dog went through the process.

Choosing an assessor

Google maintains a list of authorized CASA assessors. We used TAC Security and their ESOF AppSec ADA platform. Cost was around $550. Some assessors charge more.

The process:

  1. Submit your app URL and OAuth scopes to the assessor
  2. They run an automated OWASP ZAP scan against all your endpoints
  3. You get a report with findings categorized by severity
  4. Fix the findings, request a rescan
  5. Once clean, they issue a Letter of Validation
  6. Submit the LOV to Google via the API verification form

Turnaround was about a week from initial scan to LOV. The scan itself runs in a day.

What the scan actually found

Our first scan scored 9.7 out of 10. Ten findings total: 1 Low severity, 9 Informational. All were headers and configuration — no critical vulnerabilities.

Here's the exact list:

Finding Severity Fix
Proxy Disclosure (CWE-204) Low Block TRACE/TRACK methods
X-Content-Type-Options Missing Info Add nosniff header
Content-Type Header Missing Info Ensure all responses have Content-Type
Application Error Disclosure Info Sanitize error responses
Strict-Transport-Security Not Set Info Add HSTS header
Suspicious Comments (TODO/DEBUG) Info Remove dev comments from production
Cache-Control Directives Info Cache-Control: no-store on tokens
Storable but Non-Cacheable Content Info Explicit cache directives on API responses
User Agent Fuzzer Info Consistent error handling across UAs
User Controllable HTML Attribute Info Escape user input in HTML attributes

If you're building on Cloudflare Workers with Hono (like we are), most of these are a single middleware:

// Security headers middleware
app.use('*', async (c, next) => {
  await next();
  c.header('X-Content-Type-Options', 'nosniff');
  c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  c.header('X-Frame-Options', 'DENY');
  c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
  c.header('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");
});

// Block TRACE/TRACK
app.use('*', async (c, next) => {
  if (c.req.method === 'TRACE' || c.req.method === 'TRACK') {
    c.status(405);
    return c.text('Method Not Allowed');
  }
  return next();
});

For the landing page (static assets on Cloudflare Pages), add a _headers file in your public directory:

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data:; frame-ancestors 'none'

Beyond the scan: what ZAP doesn't catch

The ZAP scan is a DAST tool — it probes your app from the outside. It's good at finding missing headers and obvious misconfigurations. It doesn't read your source code.

After passing the scan, we did a deeper code review against OWASP ASVS v4.0 and found issues the scan would never catch:

  • Timing-safe comparisons: All our secret comparisons used !==. An attacker could measure response times to brute-force secrets byte by byte. We wrote a timingSafeEqual() utility using crypto.subtle HMAC comparison.
  • Open redirect via redirect_uri: We weren't validating the redirect_uri parameter against a per-client allowlist. An attacker could steal auth codes by specifying their own callback URL. We added a redirectUris field to API keys, validated on every /oauth/authorize call.
  • Webhook replay: Our Stripe webhook handler didn't validate the timestamp or check for duplicate events. A replayed webhook could double-credit an account. We added timestamp validation (reject events >5 minutes old) and idempotency via session.id deduplication in KV.
  • No rate limiting: Zero rate limiting on key creation or token exchange. We added KV-based rate limiting (5/min for key creation, 20/min for token exchange).
  • No data deletion: Google requires a way for users to delete their data. We had no DELETE endpoint for API keys.

We documented all of this in a CASA-CHECKLIST.md in our repo — 10 scan findings (Layer 1) and 28 code-level findings (Layer 2), with status tracking for each.

The actual cost and timeline

Item Cost
TAC Security assessment ~$550
Google Cloud project setup $0
OAuth consent screen review $0 (but weeks of back-and-forth)
Annual recertification ~$550/year
Total (first year) ~$550 + your time

Timeline: about 2-3 weeks from "I need Gmail access" to a production-approved OAuth consent screen, assuming you already have a working app. Most of that time is waiting on Google's review, not the CASA assessment itself.

Should you self-host or use a service?

inbox.dog is open source — you can self-host the entire OAuth proxy. But the CASA audit is per-application. If you self-host, you need your own audit.

The math is simple: at $0.10 per OAuth flow on the hosted version, $550 buys you 5,500 flows. If you need fewer than that, the hosted version is cheaper. If you need more, self-host and pay for the audit yourself.

Either way, you skip the Google Cloud Console setup, OAuth consent screen review, and months of back-and-forth with Google's trust and safety team.

Checklist for your own CASA audit

If you're going through this yourself, here's the minimum to pass the ZAP scan:

  1. Set X-Content-Type-Options: nosniff on all responses
  2. Set Strict-Transport-Security: max-age=31536000; includeSubDomains
  3. Set X-Frame-Options: DENY
  4. Set Content-Security-Policy (at minimum frame-ancestors 'none')
  5. Set Referrer-Policy: strict-origin-when-cross-origin
  6. Block TRACE and TRACK HTTP methods
  7. Return Cache-Control: no-store on any response containing tokens or secrets
  8. Sanitize error responses — no stack traces, no internal details
  9. Remove TODO/FIXME/DEBUG comments from production-served HTML and JS
  10. Escape any user-controlled values rendered in HTML

And what the scan won't catch but you should fix anyway:

  • Use timing-safe comparison for all secret/HMAC checks
  • Validate redirect_uri against a per-client allowlist
  • Add idempotency to payment webhooks
  • Validate webhook timestamps
  • Rate limit key creation and token exchange
  • Provide a data deletion endpoint

Our full checklist with status tracking is at CASA-CHECKLIST.md.

References