Diagram

I run this blog on Cloudflare Pages — static HTML, no server, no backend. But there are things I want to put behind authentication: admin dashboards, internal tools, staging previews. The conventional answer is “set up Google Workspace SSO.” That costs money, requires domain verification, and ties you to a paid Google tenant.

It turns out you do not need any of that. Cloudflare Access works with free Google OAuth — a standard Google Cloud Console OAuth Client ID, connected to a plain Gmail account. No Workspace. No domain verification. No cost. You can also use GitHub OAuth, which is even simpler.

Here is how to set it up, end to end.

Part 1: The Architecture

Diagram

Three things happen:

  1. Cloudflare intercepts the request before it reaches your origin. If there is no valid CF_Authorization cookie, the user sees the Cloudflare Access login page instead of your site.

  2. The user authenticates against Google or GitHub. Cloudflare never sees their password — it just receives an OAuth authorization code and exchanges it for an identity token. The IdP handles all the credential verification.

  3. Cloudflare evaluates your Access policy. If the user’s email domain matches, or they belong to the right GitHub organization, Cloudflare mints a signed JWT, sets it as a cookie, and forwards the request to your origin. The origin receives a Cf-Access-Jwt-Assertion header with the validated user identity.

Your origin does not need to implement authentication. Cloudflare does it at the edge, before any byte of your application is served.

Part 2: Google OAuth — Free, No Workspace

This is the path that surprises people. You do not need Google Workspace. You do not need a verified domain. You just need a Google Cloud project with an OAuth consent screen configured for External audience.

Step 1: Create a Google Cloud Project

Go to the Google Cloud Console. Create a new project. Name it whatever you want — it is just an internal label.

Navigate to APIs & Services → OAuth consent screen. Choose External as the user type. This is the critical setting — “External” means any Google account can authenticate, not just accounts in your Workspace organization.

Fill in the required fields:

Field Value
App name Your site name (visible to users on the consent screen)
User support email Your Gmail address
Developer contact Your Gmail address
Scopes email, profile, openid (default)

Do not add sensitive scopes. The default email/profile/openid scopes do not require Google verification. If you add scopes like calendar.readonly or drive.file, Google will require you to verify your app — a process that can take weeks.

Save and continue. On the “Test users” step, add your own Gmail address as a test user while the app is in testing mode. You can publish it later, but testing mode is sufficient for your own use.

Step 3: Create the OAuth Client ID

Go to APIs & Services → Credentials → Create Credentials → OAuth client ID. Choose Web application.

Two fields matter:

Field Value
Authorized JavaScript origins https://<your-team-name>.cloudflareaccess.com
Authorized redirect URIs https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback

Your team name is visible in the Cloudflare Zero Trust dashboard under Settings → Team Domain. It looks like myteam.cloudflareaccess.com.

Copy the Client ID and Client Secret. You will paste them into Cloudflare.

Step 4: Add Google as an Identity Provider in Cloudflare

In the Cloudflare dashboard, go to Zero Trust → Settings → Authentication → Identity Providers → Add new → Google.

Paste your Client ID and Client Secret. Click Save. That is it.

Diagram

Common Pitfall: “Access blocked: app not verified”

If you see this during login, your OAuth consent screen is in testing mode and the Google account you are logging in with is not on the test users list. Go back to OAuth consent screen → Test users and add the email address.

Alternatively, click Publish App on the consent screen to make it available to all Google users. Publishing does not require verification as long as you only use the default scopes.

Part 3: GitHub OAuth — Even Simpler

If your audience is developers, GitHub OAuth is the path of least resistance. No cloud console, no consent screen, no test users. Just an OAuth App registration.

Step 1: Register a GitHub OAuth App

Go to GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.

Field Value
Application name Your site name
Homepage URL https://<your-team-name>.cloudflareaccess.com
Authorization callback URL https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback

Register the app. Copy the Client ID. Click Generate a new client secret and copy it.

Step 2: Add GitHub as an Identity Provider in Cloudflare

Zero Trust → Settings → Authentication → Identity Providers → Add new → GitHub. Paste the Client ID and Client Secret. Save.

That is the entire setup. GitHub OAuth has zero configuration beyond those two fields.

What GitHub Claims Are Available

When a user authenticates via GitHub, Cloudflare Access receives these claims from the GitHub OAuth token:

Claim Example Use In Policy
username nurazhardotcom Allow specific users
email [email protected] Allow by email
organizations my-org Allow org members
teams my-org/engineering Allow team members

These claims become available as Access policy selectors. You can write a policy that says “Allow if GitHub organization is my-company” — and only members of that GitHub org get through.

Part 4: Writing Access Policies

An Access policy is a set of rules that determine who gets in. Every policy has an Action (Allow, Block, or Bypass), one or more Rules (Include, Require, Exclude), and Selectors that match against identity provider claims.

Policy Examples

Allow specific email addresses:

Action: Allow
Include → Selector: Emails → Value: [email protected], [email protected]

Allow an entire email domain (free Gmail):

Action: Allow
Include → Selector: Emails ending in → Value: @gmail.com

Allow a GitHub organization:

Action: Allow
Include → Selector: GitHub Organization → Value: my-company

Allow GitHub org members OR specific email:

Action: Allow
Include → Selector: GitHub Organization → Value: my-company
--- OR ---
Include → Selector: Emails → Value: [email protected]

Require both a specific email domain AND a specific GitHub org (defense in depth):

Action: Allow
Include → Selector: Emails ending in → Value: @my-company.com
Require → Selector: GitHub Organization → Value: my-company

The difference between Include and Require: - Include rules are OR’d — satisfy any one to pass - Require rules are AND’d — must satisfy ALL Require rules in addition to at least one Include rule

Multiple Identity Providers

You can enable both Google and GitHub simultaneously. When a user hits the login page, they see both options:

┌─────────────────────────────┐
│       Cloudflare Access      │
│                              │
│   [Login with Google]       │
│   [Login with GitHub]       │
│                              │
└─────────────────────────────┘

Policies can reference claims from either IdP. A user who logs in via Google must satisfy the Google-based rules. A user who logs in via GitHub must satisfy the GitHub-based rules. You can mix and match freely.

Part 5: Protecting Your Site

For Cloudflare Pages

If your site is hosted on Cloudflare Pages (like nurazhar.com):

  1. Go to Zero Trust → Access → Applications → Add an application
  2. Choose Self-hosted
  3. Set the application domain to your Pages custom domain (e.g., admin.nurazhar.com)
  4. Attach your Access policy
  5. Done

The first visitor to admin.nurazhar.com sees the Cloudflare Access login page instead of your site content. After authentication, they get the CF_Authorization cookie and subsequent requests pass through transparently.

Important: Cloudflare Pages serves your site from the edge. There is no origin server to validate JWTs against. The Access policy enforcement happens entirely at the Cloudflare edge — your static HTML files never know authentication is happening. This means you cannot use the Cf-Access-Jwt-Assertion header for per-user customization, since there is no backend to read it. If you need per-user content on a static site, use Cloudflare Workers or Pages Functions as a middleware layer.

For Self-Hosted Applications (Docker, VPS, Homelab)

If you are protecting a backend service:

  1. Create a Cloudflare Tunnel (cloudflared) pointing to your service
  2. Create a Self-hosted Access application with the tunnel’s public hostname
  3. Enable Protect with Access on the tunnel configuration

The tunnel daemon validates the JWT before forwarding traffic to your origin. Requests without a valid token never reach your server — they are rejected at the tunnel level.

Verifying the JWT on Your Backend

If your backend needs to know who is authenticated, it can validate the JWT locally. Cloudflare publishes its public signing keys at:

https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certs

A minimal Node.js verifier:

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: `https://${TEAM_DOMAIN}/cdn-cgi/access/certs`
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(null, key.getPublicKey());
  });
}

function verifyToken(req) {
  const token = req.cookies['CF_Authorization'] ||
                req.headers['cf-access-jwt-assertion'];
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      issuer: `https://${TEAM_DOMAIN}`,
      audience: APP_AUD
    }, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });
}

// Usage:
const claims = await verifyToken(req);
console.log(claims.email); // [email protected]

The decoded JWT contains:

{
  "email": "[email protected]",
  "name": "User Name",
  "sub": "abc123",
  "iss": "https://myteam.cloudflareaccess.com",
  "aud": "abc123def456",
  "exp": 1690000000,
  "iat": 1689900000,
  "identity_provider": "google",
  "identity_provider_id": "abc-def-ghi"
}

Part 6: Free Tier — What You Get

Cloudflare Zero Trust’s free tier is genuinely generous:

Feature Free Tier Paid (Pay-as-you-go)
Users Up to 50 Unlimited ($7/user/month)
Identity providers Unlimited Unlimited
Applications Unlimited Unlimited
Access policies Unlimited Unlimited
JWT signing RS256 RS256
Log retention 24 hours 30 days
Device posture Basic Advanced
mTLS No Enterprise
Browser isolation No Enterprise

For a personal blog, a small team internal tool, or a homelab dashboard, the free tier covers everything you need. Fifty users is more than enough for most small-scale deployments.

The main limitation is log retention — 24 hours is tight for security auditing. If you need longer retention, you can stream Access logs to external storage via Logpush.

Part 7: Security Considerations

What this protects against: - Random internet users accessing your internal tools - Search engine indexing of private pages - Unauthenticated API calls to protected endpoints - Credential-stuffing attacks (Google/GitHub handle that)

What this does NOT protect against: - A compromised Google or GitHub account (enable 2FA on your IdP) - Token replay if the JWT cookie is stolen via XSS (the cookie is HttpOnly by default, mitigating this) - Attacks that bypass Cloudflare entirely by hitting your origin IP directly (use Cloudflare Tunnel or IP allowlisting)

Hardening recommendations:

  1. Enable the Binding Cookie (CF_Binding). This ties the CF_Authorization JWT to the specific browser session, preventing stolen cookies from being reused from a different device.

  2. Set session duration appropriately. The default is 24 hours. For sensitive tools, reduce to 15 minutes or less. Cloudflare Access supports configurable session durations per application.

  3. Use Require rules for defense in depth. A policy that requires BOTH a specific email domain AND GitHub org membership is harder to bypass than either alone.

  4. Monitor Access logs in the Zero Trust dashboard. Look for unusual authentication patterns, repeated failures, or access from unexpected countries.

The Bottom Line

You can put enterprise-grade authentication in front of any site — static JAMstack, Docker container, homelab dashboard, internal wiki — without paying a cent. Google OAuth works with a plain Gmail account. GitHub OAuth works with a personal GitHub account. Cloudflare Access handles the JWT minting, cookie management, and policy evaluation at the edge.

The entire setup takes about 10 minutes: - 5 minutes to create the Google Cloud OAuth client or GitHub OAuth app - 2 minutes to paste credentials into Cloudflare - 3 minutes to write an Access policy

And then your site is behind authentication. For free. For up to 50 users. Forever.