Guide

Gmail OAuth2 in PowerShell

Google deprecated Gmail basic auth in September 2024 and now requires OAuth2 for all programmatic access. The manual flow involves 6 steps: GCP project creation, API enablement, credential generation, consent URL construction, token exchange, and refresh logic. This guide covers what's actually happening under the hood and shows how Nylas CLI handles all of it. Also covers service accounts vs user OAuth, Workspace admin consent, token rotation, and security best practices. Same CLI works with Outlook, Exchange, Yahoo, iCloud, and IMAP.

By Caleb Geene

How Gmail OAuth2 works (the 6-step flow)

Understanding the flow helps you debug issues. According to Google's OAuth2 documentation, the standard authorization code flow for Gmail involves these steps:

  1. Register an OAuth app in Google Cloud Console -- create a project, enable the Gmail API, configure the consent screen, create OAuth 2.0 credentials
  2. Build the consent URL with your client ID, redirect URI, scopes, and access_type=offline
  3. User authenticates in the browser -- Google shows a consent screen listing requested permissions
  4. Exchange the auth code for an access token (valid 3600 seconds) and a refresh token
  5. Store tokens securely -- access tokens in memory, refresh tokens encrypted at rest
  6. Refresh when expired -- POST to Google's token endpoint with the refresh token

Here's what the manual flow looks like in PowerShell:

# THE MANUAL WAY (shown for understanding -- don't do this)

# Step 1: GCP project setup (10+ minutes in browser)
# - Create project at console.cloud.google.com
# - Enable Gmail API
# - Configure OAuth consent screen
# - Create OAuth 2.0 Client ID

# Step 2: Build consent URL
$clientId = "123456789.apps.googleusercontent.com"
$clientSecret = "GOCSPX-xxxxx"  # secret in your script!
$scope = "https://www.googleapis.com/auth/gmail.modify"
$authUrl = "https://accounts.google.com/o/oauth2/v2/auth" +
    "?client_id=$clientId&redirect_uri=http://localhost:8080" +
    "&response_type=code&scope=$scope&access_type=offline"

# Step 3: Open browser, catch redirect
Start-Process $authUrl
$listener = [System.Net.HttpListener]::new()
$listener.Prefixes.Add("http://localhost:8080/")
$listener.Start()
$code = $listener.GetContext().Request.QueryString["code"]
$listener.Stop()

# Step 4: Exchange code for tokens
$tokens = Invoke-RestMethod -Uri "https://oauth2.googleapis.com/token" `
    -Method POST -Body @{
        code=$code; client_id=$clientId; client_secret=$clientSecret
        redirect_uri="http://localhost:8080"; grant_type="authorization_code"
    }
# $tokens.access_token expires in 3600 seconds
# $tokens.refresh_token must be stored securely

# Step 5-6: Refresh logic needed in EVERY script
# ...50+ lines of boilerplate per script

The Nylas CLI approach: one command

Nylas CLI handles steps 1-6 above. One command, no GCP project, no client secrets in your scripts.

# Install (see cli.nylas.com for all install methods)
irm https://cli.nylas.com/install.ps1 | iex

# Authenticate with Gmail (interactive -- opens browser)
nylas auth login
# Select Google, sign in, approve. Done.

# Verify
nylas auth whoami
# Email: you@gmail.com
# Provider: google
# Grant ID: grant_abc123
# Status: active

How token rotation works

Google's OAuth2 access tokens expire after exactly 3600 seconds (1 hour). According to Google's Identity documentation, refresh tokens don't expire unless the user revokes access, changes their password, or the token hasn't been used in 6 months.

Nylas CLI handles this transparently:

  • Before every command, the CLI checks if the access token is expired or near-expiry
  • If expired, it exchanges the refresh token for a new access token
  • Token storage -- on Windows, tokens go to Windows Credential Manager; on macOS, Keychain; on Linux, a secured config file
  • No intervention needed -- your scripts never see auth errors from expired tokens
# This works even days after your last CLI call
# Token refresh happens automatically before the API call
nylas email list --limit 5

# Verify token status anytime
nylas auth whoami

Headless authentication (CI/CD, servers)

Interactive nylas auth login opens a browser, which doesn't work in headless environments. For CI/CD pipelines, Docker containers, and servers, use API key authentication.

# Get your API key from dashboard-v3.nylas.com
# Store it as an environment variable (never hardcode)

# Headless authentication with API key
nylas auth config --api-key $env:NYLAS_API_KEY

# Verify
nylas auth whoami

# GitHub Actions example:
#   env:
#     NYLAS_API_KEY: ${{ secrets.NYLAS_API_KEY }}
#   run: nylas auth config --api-key $env:NYLAS_API_KEY

# Azure DevOps example:
#   env:
#     NYLAS_API_KEY: $(NYLAS_API_KEY)
#   script: nylas auth config --api-key $env:NYLAS_API_KEY

Service accounts vs user OAuth

Google offers two authentication paths. The right choice depends on your use case.

FeatureUser OAuthService Account
AuthenticationUser signs in via browserJSON key file (no browser)
Token managementRefresh token per userJWT signed per request
Access scopeSingle user's mailboxAny user via delegation
Workspace admin neededNo (unless restricted)Yes (must configure delegation)
Use casePersonal scripts, dev toolsServer-to-server automation
Nylas CLI equivalentnylas auth loginnylas auth config --api-key

For personal Gmail or small team usage, nylas auth login is simpler. For organization-wide automation, API key authentication handles server-to-server flows.

Workspace organizations can restrict third-party app access. According to Google's Workspace admin documentation, there are three consent models:

  1. User consent (default for consumer Gmail) -- each user approves individually
  2. Admin-managed access -- the admin pre-approves apps; users skip the consent screen
  3. Domain-wide delegation -- a service account impersonates any user without individual consent

If users see "This app is blocked" during nylas auth login:

# Solution: Ask your Workspace admin to:
# 1. Go to admin.google.com > Security > API Controls > App Access Control
# 2. Search for "Nylas" in the app catalog
# 3. Set access to "Trusted" or "Limited"

# After admin approval, users authenticate normally:
nylas auth login
# The consent screen shows org branding instead of a block warning

Manage multiple Gmail accounts

# Authenticate work and personal Gmail
nylas auth login   # work@company.com
nylas auth login   # personal@gmail.com

# List all authenticated accounts
nylas auth list
# GRANT ID          EMAIL                PROVIDER  STATUS
# grant_abc123      work@company.com     google    active
# grant_def456      personal@gmail.com   google    active

# Target a specific account
nylas email list --grant grant_def456 --limit 5

# Send from a specific account
nylas email send --grant grant_abc123 `
    --to "client@example.com" `
    --subject "Proposal" --body "See attached." --yes

# Revoke access for one account
nylas auth revoke grant_def456

Gmail API scopes explained

Google defines specific OAuth2 scopes that control access. According to Google's Gmail API reference:

ScopeAccess levelUsed for
gmail.readonlyRead-only mailbox accessemail list, search, read
gmail.sendSend on behalf of useremail send
gmail.modifyRead/write (no delete)All email commands
gmail.labelsManage labelsFolder operations
calendar.readonlyRead calendar eventscalendar events list
calendar.eventsCreate/edit eventscalendar events create

Nylas CLI requests the minimum scopes needed. You don't configure scopes manually.

Security best practices

# GOOD: Environment variables for API keys
nylas auth config --api-key $env:NYLAS_API_KEY

# BAD: Hardcoded key in script
# nylas auth config --api-key "nylas_v3_abc123..."  # NEVER DO THIS

# GOOD: Use CI/CD secret managers
#   GitHub: Settings > Secrets > Actions
#   Azure DevOps: Variable Groups (secret type)
#   Jenkins: Credentials store

# GOOD: Verify auth before running scripts
$auth = nylas auth whoami --json 2>&1
if ($LASTEXITCODE -ne 0) {
    Write-Host "Not authenticated." -ForegroundColor Red
    exit 1
}

# GOOD: Revoke access when no longer needed
nylas auth revoke grant_abc123

Troubleshoot common issues

# "This app is blocked" on consent screen
# Fix: Workspace admin approves Nylas at admin.google.com > Security > API Controls

# Refresh token revoked unexpectedly
# Cause: Password changed, or token unused 6+ months
# Fix: Re-authenticate with 'nylas auth login'

# "Quota exceeded" errors
# Cause: Gmail API daily quota (1 billion units)
# Fix: Add Start-Sleep between rapid calls; normal usage won't hit this

# Verify auth status
nylas auth whoami
# If "active", your tokens are valid

Frequently asked questions

Do I need a Google Cloud Console project for Gmail OAuth with Nylas CLI?

No. Nylas CLI handles the OAuth flow through Nylas' registered application. Run nylas auth login, authenticate with Google, and the CLI stores tokens securely. No GCP project, no client secrets.

How does Nylas CLI handle Gmail refresh token rotation?

Google's access tokens expire after 3600 seconds. Nylas refreshes them automatically before each command. Refresh tokens persist until the user revokes access or changes their password.

What's the difference between service accounts and user OAuth?

User OAuth requires a browser sign-in for a single user. Service accounts use a JSON key file for server-to-server auth and can impersonate any user via domain-wide delegation. Nylas CLI uses nylas auth login for user OAuth and nylas auth config --api-key for headless workflows.

How do I set up Gmail access for a Workspace organization?

A Workspace admin must approve Nylas at admin.google.com > Security > API Controls. After approval, users authenticate with nylas auth login without seeing a block screen.


Next steps