Source: https://cli.nylas.com/guides/oauth-pkce-for-email-explained

# OAuth PKCE for Email, Explained

A native or CLI email app can't keep a client secret, so the classic authorization-code flow leaves a gap: anyone who intercepts the redirected code can trade it for a token. PKCE (RFC 7636) closes that gap with a per-request code_verifier and a hashed code_challenge. This guide explains what PKCE adds, why public clients need it, how Google and Microsoft require it for native apps, and how Nylas runs the whole exchange for you.

Written by [Aaron de Mello](https://cli.nylas.com/authors/aaron-de-mello) Senior Engineering Manager

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** PKCE (“pixie”) adds a one-time secret to the OAuth authorization-code flow. Your app generates a random `code_verifier`, sends its SHA-256 hash as the `code_challenge`, then proves it holds the original verifier when redeeming the code. A stolen authorization code is useless without it. Read on for the one command that runs the entire exchange — without you ever touching a verifier.

Command references used in this guide: [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login), [`nylas auth status`](https://cli.nylas.com/docs/commands/auth-status), and [`nylas auth scopes`](https://cli.nylas.com/docs/commands/auth-scopes).

## What is OAuth PKCE?

PKCE (Proof Key for Code Exchange) is an extension to the OAuth 2.0 authorization-code flow, defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) in September 2015. It binds an authorization request to the client that started it, using a one-time secret the client never transmits in the clear. That binding stops an attacker who intercepts the redirected code from redeeming it.

The mechanism has three values. The client invents a high-entropy random string called the `code_verifier` (43 to 128 characters per the RFC). It derives a `code_challenge` by SHA-256 hashing the verifier and base64url-encoding the result — the `S256` method. The authorization request carries the challenge; the token request carries the verifier. The server hashes the verifier and rejects the exchange unless it matches the challenge it stored.

## Why do public email clients need PKCE?

A public client is an app that cannot keep a secret: a CLI, a desktop mail client, a mobile app, or a single-page web app. Its code ships to the user, so any embedded client secret is extractable. Without PKCE, the authorization code returned to the redirect URI is the only thing standing between an attacker and a token — and on a shared machine or a hijacked custom-URI scheme, that code can leak.

PKCE replaces the missing client secret with a dynamic, per-request proof. Because the `code_verifier` never leaves the originating client and is generated fresh each time, a leaked authorization code is worthless to anyone who lacks it. RFC 6749 originally treated authorization-code interception as a confidential-client problem; the [OAuth 2.0 Threat Model (RFC 6819)](https://datatracker.ietf.org/doc/html/rfc6819) documents code interception as a named attack, and PKCE is the mitigation. The draft [OAuth 2.1](https://oauth.net/2.1/) consolidation makes PKCE mandatory for all clients using the authorization-code flow, not just public ones.

## How does the PKCE flow work step by step?

The PKCE flow adds two fields to the standard exchange and one verification step on the server. The client generates the secret up front, hashes it for the authorization request, then reveals the original only when trading the code for tokens. Four steps run between the user clicking “connect” and your app holding an access token, and the verifier check is the new one.

1. Generate a random `code_verifier` and compute `code_challenge = BASE64URL(SHA256(verifier))`.
2. Send the user to the authorization endpoint with `code_challenge` and `code_challenge_method=S256`.
3. After consent, the provider redirects back with a short-lived authorization code.
4. Exchange the code at the token endpoint, including the original `code_verifier`; the server hashes it and compares.

The example below shows the conceptual values an OAuth client computes. The `openssl` commands generate a compliant verifier and its S256 challenge so you can see the relationship between the two fields. RFC 7636 requires the verifier to use only unreserved URI characters, which is why base64url encoding (not standard base64) is used.

```bash
# 1. code_verifier: 32 random bytes, base64url-encoded (43 chars)
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='

# 2. code_challenge: SHA-256 of the verifier, base64url-encoded
printf '%s' "$VERIFIER" \
  | openssl dgst -binary -sha256 \
  | openssl base64 | tr '+/' '-_' | tr -d '='

# Authorization request then carries:
#   code_challenge=<challenge>&code_challenge_method=S256
# Token request then carries:
#   code=<auth_code>&code_verifier=<verifier>
```

## How does the CLI handle PKCE for you?

The Nylas CLI runs the entire PKCE exchange during `nylas auth login`. It generates the verifier, computes the S256 challenge, opens the provider consent screen, captures the redirect on a local loopback port, and redeems the code with the verifier — you never construct a single field. One command connects Gmail, Outlook, or Exchange, and the tool stores the resulting grant in your system keyring.

Because the flow is identical from your side regardless of backend, switching providers is a one-flag change. Google requires PKCE for installed apps per its [native-app OAuth guide](https://developers.google.com/identity/protocols/oauth2/native-app), and Microsoft requires it for public clients per the [Microsoft identity platform auth-code docs](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow). The CLI satisfies both with the same code path, so you don't maintain two implementations.

```bash
# Connect Gmail; the CLI runs the full PKCE authorization-code flow
nylas auth login --provider google

# Connect Outlook with the same command, just a different provider
nylas auth login --provider microsoft

# Confirm the grant landed and inspect granted scopes
nylas auth status
nylas auth scopes
```

## How do you confirm the PKCE login succeeded?

After `nylas auth login` completes the PKCE exchange, confirm the result with `nylas auth status` and `nylas auth scopes`. The first reports the connected account and grant state; the second lists the permissions the token actually carries. If the browser closed before redirecting, no grant is created and the status check shows nothing connected — rerun the login.

A common failure is a verifier mismatch, which surfaces as an `invalid_grant` error at the token endpoint. Authorization codes are single-use and short-lived (Google expires them in roughly 10 minutes), so a stale code or a retried exchange fails the S256 check. The fix is the same in every case: rerun `nylas auth login` to start a fresh flow with a new verifier.

```bash
# Machine-readable status for scripts and CI checks
nylas auth status --json

# List the scopes the active grant holds
nylas auth scopes
```

## Next steps

- [OAuth scopes for email](https://cli.nylas.com/guides/oauth-scopes-for-email-explained) — what each scope grants and least privilege
- [Email API authentication methods](https://cli.nylas.com/guides/email-api-authentication-methods) — OAuth, app passwords, and keys
- [Refresh token management](https://cli.nylas.com/guides/refresh-token-management) — how tokens expire and renew
- [Fix the invalid_grant error](https://cli.nylas.com/guides/fix-invalid-grant-error) — when a code exchange or grant fails
- [Email to GitHub issues](https://cli.nylas.com/guides/email-to-github-issues) — an OAuth-backed CLI pipeline in practice
- [Email to Google Drive](https://cli.nylas.com/guides/email-to-google-drive) — pull email JSON and push to a Drive API
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
- Standards: [RFC 7636 (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636), [RFC 6749 (OAuth 2.0)](https://datatracker.ietf.org/doc/html/rfc6749), and the [OAuth 2.1 draft](https://oauth.net/2.1/)
