惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
Vercel News
Vercel News
M
MIT News - Artificial intelligence
L
LangChain Blog
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Azure Blog
Microsoft Azure Blog
J
Java Code Geeks
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
IT之家
IT之家
F
Fortinet All Blogs
博客园 - 聂微东
U
Unit 42
Martin Fowler
Martin Fowler
腾讯CDC
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to set up refresh-token-only OAuth for a multi-tenant...
foxck016077 · 2026-05-20 · via DEV Community

How to set up refresh-token-only OAuth for a multi-tenant Apify Actor (Gmail, 10 minutes)

If you're shipping an Apify Actor that calls a per-user Google API (Gmail, Calendar, Drive) and you want the simplest auth model that works for cold strangers, this is the setup.

The pattern: the buyer pastes three strings — refresh_token, client_id, client_secret — into the Actor input. The Actor exchanges them for a short-lived access token at runtime, hits the API, exits. No mailbox cache, no per-user OAuth callback URL, no Apify-side identity storage.

You can finish this in 10 minutes including the Google Cloud setup. I'll show the working version end-to-end. This is the actual pattern I'm running in production on apify.com/foxck/gmail-inbox-intel.

Step 1 — Google Cloud setup (5 min)

  1. Open console.cloud.google.com, create a new project or pick an existing one.
  2. APIs & Services → Enable APIs → enable Gmail API (or whichever Google API you need).
  3. APIs & Services → OAuth consent screen → External, fill in app name + your email, add the scope you actually need (e.g. gmail.readonly). If you're staying under 100 users, leave the app in "Testing" mode and add test users manually — no Google verification required.
  4. APIs & Services → Credentials → Create credentials → OAuth client ID → Desktop app. Download the JSON. You now have client_id and client_secret.

The "Desktop app" type is the unlock here. It does not require you to host an OAuth redirect URL. Google's library will spin up localhost:8080 during the consent flow and capture the code automatically.

Step 2 — Generate the refresh token (2 min)

The buyer runs this once on their own machine. You don't host this, they do.

# scripts/oauth_setup.py
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=8080, access_type="offline", prompt="consent")
print("refresh_token:", creds.refresh_token)

Enter fullscreen mode Exit fullscreen mode

access_type="offline" + prompt="consent" is what guarantees Google returns a refresh token. Without prompt="consent", if the user has authorized this client before, Google will return only an access token. With it, the consent screen reappears and a fresh refresh token gets minted.

Output: one long string starting 1//. That's the refresh_token. They paste it into the Actor input alongside client_id and client_secret.

Step 3 — Exchange for an access token at runtime (1 min)

In the Actor's main handler, before hitting Gmail:

import requests

def get_access_token(client_id, client_secret, refresh_token):
    resp = requests.post(
        "https://oauth2.googleapis.com/token",
        data={
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": refresh_token,
            "grant_type": "refresh_token",
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

Enter fullscreen mode Exit fullscreen mode

That access token is good for ~1 hour. For an Actor that runs in seconds-to-minutes, one exchange per run is enough — no caching needed.

Step 4 — Define the Actor input schema (1 min)

INPUT_SCHEMA.json:

{
  "title": "Gmail Actor input",
  "type": "object",
  "required": ["client_id", "client_secret", "refresh_token"],
  "properties": {
    "client_id": { "type": "string", "title": "Google client_id", "editor": "textfield" },
    "client_secret": { "type": "string", "title": "Google client_secret", "editor": "textfield", "isSecret": true },
    "refresh_token": { "type": "string", "title": "Google refresh_token", "editor": "textfield", "isSecret": true }
  }
}

Enter fullscreen mode Exit fullscreen mode

The isSecret: true flag tells the Apify UI to mask the field at rest in the Actor run record. Apify auto-encrypts secret input fields with platform-level keys.

Step 5 — Bonus: a dry-run mode so buyers can test without OAuth (1 min)

The biggest setup-killer for a cold buyer is the Google Cloud Console + OAuth client + consent screen detour in step 1. They land on your Actor, click Run, hit the OAuth wall, give up.

Cure: an optional dry_run boolean. When true, the Actor skips the OAuth exchange entirely and emits a synthetic dataset matching your real schema:

async def main():
    inp = await Actor.get_input()
    if inp.get("dry_run"):
        await Actor.push_data(SYNTHETIC_SAMPLE)
        return
    # ... normal flow with OAuth ...

Enter fullscreen mode Exit fullscreen mode

Now the buyer's first interaction is "Click Run on the public example → see what the output looks like." No Cloud Console. They commit to the OAuth flow only after they've seen the JSON shape and decided they want it.

Why this pattern, not the alternatives

  • Apify Integrations OAuth: tying the Actor to Apify's identity store would lock self-host buyers out of the same source.
  • Service Account + domain-wide delegation: works for one Google Workspace, not multi-tenant strangers.
  • Per-user OAuth callback URL hosted somewhere: extra infra, extra cost, extra failure surface.

Refresh-token-only OAuth shifts the trust boundary cleanly: the buyer holds their own credentials, the Actor is stateless, both Apify and self-host modes work from the same code.

The repo

Full source running this pattern in production: github.com/foxck016077/apify-gmail-inbox-intel (MIT). The Actor lives at apify.com/foxck/gmail-inbox-intel — click "Try for free" with the dry_run: true input and you'll see the output shape without doing the OAuth setup.

If you're building something similar and the pattern's unclear, the AMA discussion is open.