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:
- Register an OAuth app in Google Cloud Console -- create a project, enable the Gmail API, configure the consent screen, create OAuth 2.0 credentials
- Build the consent URL with your client ID, redirect URI, scopes, and
access_type=offline - User authenticates in the browser -- Google shows a consent screen listing requested permissions
- Exchange the auth code for an access token (valid 3600 seconds) and a refresh token
- Store tokens securely -- access tokens in memory, refresh tokens encrypted at rest
- 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 scriptThe 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: activeHow 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 whoamiHeadless 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_KEYService accounts vs user OAuth
Google offers two authentication paths. The right choice depends on your use case.
| Feature | User OAuth | Service Account |
|---|---|---|
| Authentication | User signs in via browser | JSON key file (no browser) |
| Token management | Refresh token per user | JWT signed per request |
| Access scope | Single user's mailbox | Any user via delegation |
| Workspace admin needed | No (unless restricted) | Yes (must configure delegation) |
| Use case | Personal scripts, dev tools | Server-to-server automation |
| Nylas CLI equivalent | nylas auth login | nylas 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.
Google Workspace admin consent
Workspace organizations can restrict third-party app access. According to Google's Workspace admin documentation, there are three consent models:
- User consent (default for consumer Gmail) -- each user approves individually
- Admin-managed access -- the admin pre-approves apps; users skip the consent screen
- 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 warningManage 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_def456Gmail API scopes explained
Google defines specific OAuth2 scopes that control access. According to Google's Gmail API reference:
| Scope | Access level | Used for |
|---|---|---|
gmail.readonly | Read-only mailbox access | email list, search, read |
gmail.send | Send on behalf of user | email send |
gmail.modify | Read/write (no delete) | All email commands |
gmail.labels | Manage labels | Folder operations |
calendar.readonly | Read calendar events | calendar events list |
calendar.events | Create/edit events | calendar 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_abc123Troubleshoot 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 validFrequently 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
- Parse and extract data from emails -- extract orders, tickets, structured data
- Send email from PowerShell -- attachments, scheduling, HTML bodies
- Replace Send-MailMessage -- migrate from the deprecated cmdlet
- Full command reference -- every flag and subcommand