Authentication API Specification¶
Deprecated (v1.x): The protocol described in the main body of this document (session-cookie + per-command browser flow) is superseded by the v2 session-token protocol documented in the Auth Spec v2 section below. The v1 endpoints remain functional but will be removed in a future release.
This document defines the protocol for authenticating AI agents and issuing short-lived service account tokens. It is designed to be implementation-agnostic — organizations can implement this specification using their own authentication systems, access policies, and infrastructure.
Overview¶
The ExtraSuite authentication protocol enables AI agents running on user devices to obtain short-lived Google Cloud service account tokens. The protocol is designed around these principles:
- User Authentication — Verify the user's identity through your organization's authentication mechanism
- Access Control — Apply your organization's policies to determine who can obtain tokens
- Localhost Redirect — Securely deliver credentials to the CLI running on the user's device
- Short-lived Tokens — Issue tokens with limited lifetime to minimize exposure
┌──────────────┐ ┌──────────────────┐
│ AI Agent │ │ Your Auth │
│ (CLI) │ │ Server │
│ │ │ │
│ 1. Start │─────────────────────▶│ 2. Authenticate │
│ local │ GET /auth?port=N │ user │
│ server │ │ │
│ │ │ 3. Apply access │
│ │ │ policies │
│ │ │ │
│ 5. Receive │◀─────────────────────│ 4. Redirect to │
│ code │ localhost:N?code=X │ localhost │
│ │ │ │
│ 6. Exchange │─────────────────────▶│ 7. Generate │
│ code │ POST /exchange │ token │
│ │◀─────────────────────│ │
│ 8. Use │ {token, ...} │ │
│ token │ │ │
└──────────────┘ └──────────────────┘
Why This Design?¶
Problem: AI Agents Need Google API Access¶
AI agents need to call Google APIs (Sheets, Docs, Slides, Drive) on behalf of users. Traditional OAuth flows don't work well because:
- Agents run as CLI tools, not web applications
- Users don't want to grant long-lived access to agents
- Organizations want to control which employees can use this capability
Solution: Server-Mediated Token Issuance¶
Instead of the AI agent obtaining OAuth tokens directly, a trusted server:
- Authenticates the user (using your existing auth system)
- Applies access policies (your rules about who can use this)
- Issues a short-lived token for a dedicated service account
This gives organizations full control over authentication and authorization while providing agents with the tokens they need.
Protocol Specification¶
Endpoint 1: Start Authentication¶
Purpose: Initiate the authentication flow. The server should authenticate the user, apply access policies, and ultimately redirect to the CLI's local server.
Request¶
| Parameter | Type | Required | Description |
|---|---|---|---|
port | integer | Yes | Port number where the CLI is listening (1024-65535) |
Server Behavior¶
The server MUST:
- Validate the port parameter
- Reject if port is outside the valid range (1024-65535)
-
Reject if port is not a valid integer
-
Authenticate the user
- Use your organization's authentication mechanism
- This could be OAuth, SAML, SSO, session cookies, etc.
-
If user is not authenticated, initiate your auth flow
-
Apply access policies
- Check if the authenticated user is allowed to obtain tokens
- Apply any organization-specific rules (department, role, etc.)
-
Show any required interstitials (terms of service, notices, etc.)
-
Generate an authorization code
- Create a short-lived, single-use authorization code
- Associate it with the user's service account email
- Code MUST expire within 2 minutes (recommended: 120 seconds)
-
Code MUST be single-use (invalidated after first exchange)
-
Redirect to localhost
- Redirect the browser to:
http://localhost:<port>/on-authentication?code=<auth_code> - Include the authorization code as a query parameter
Responses¶
Success (Redirect):
Error - Invalid Port:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_request",
"error_description": "Port must be between 1024 and 65535"
}
Error - Access Denied:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "access_denied",
"error_description": "User is not authorized to obtain tokens"
}
Sequence Diagram¶
CLI Server Auth System
│ │ │
│ GET /auth?port=8085 │ │
│────────────────────────▶│ │
│ │ │
│ │ Authenticate user │
│ │──────────────────────────▶│
│ │ │
│ │ User identity │
│ │◀──────────────────────────│
│ │ │
│ │ Apply access policies │
│ │ (internal) │
│ │ │
│ │ Generate auth code │
│ │ (internal) │
│ │ │
│ 302 Redirect │ │
│ localhost:8085?code=X │ │
│◀────────────────────────│ │
│ │ │
Endpoint 2: Exchange Code for Token¶
Purpose: Exchange a valid authorization code for a short-lived access token.
Request¶
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The authorization code received via localhost redirect |
Server Behavior¶
The server MUST:
- Validate the authorization code
- Verify the code exists and has not expired
- Verify the code has not been used before
-
Invalidate the code immediately (single-use)
-
Generate the access token
- Create a short-lived access token for the user's service account
- Token MUST have limited lifetime (recommended: 60 minutes max)
-
Token SHOULD include appropriate scopes for Google APIs
-
Return token information
- Include the access token
- Include the expiration timestamp
- Include the service account email
Response¶
Success:
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "ya29.a0AfH6SMB...",
"expires_at": "2026-01-23T14:30:00Z",
"service_account": "user-abc@project.iam.gserviceaccount.com"
}
| Field | Type | Description |
|---|---|---|
token | string | The access token for Google API calls |
expires_at | string | ISO 8601 timestamp when the token expires |
service_account | string | Email of the service account (for sharing files) |
Error - Invalid Code:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_grant",
"error_description": "Authorization code is invalid or expired"
}
Error - Code Already Used:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_grant",
"error_description": "Authorization code has already been used"
}
Localhost Callback Format¶
The CLI starts a temporary HTTP server on localhost to receive the authorization code. The server redirects the browser to this endpoint after successful authentication.
Query Parameters¶
| Parameter | Type | Description |
|---|---|---|
code | string | Authorization code to exchange for a token |
error | string | (Optional) Error code if authentication failed |
error_description | string | (Optional) Human-readable error message |
Success Case¶
The CLI should: 1. Extract the code parameter 2. Close the local HTTP server 3. Call the token exchange endpoint 4. Cache the resulting token
Error Case¶
GET http://localhost:8085/on-authentication?error=access_denied&error_description=User%20not%20authorized
The CLI should: 1. Display the error to the user 2. Close the local HTTP server 3. Exit with an appropriate error code
Security Requirements¶
Authorization Code Requirements¶
| Requirement | Value | Rationale |
|---|---|---|
| Lifetime | ≤ 120 seconds | Minimize window for interception |
| Usage | Single-use | Prevent replay attacks |
| Entropy | ≥ 256 bits | Prevent guessing |
| Storage | Server-side only | Code never stored on client |
Access Token Requirements¶
| Requirement | Value | Rationale |
|---|---|---|
| Lifetime | ≤ 60 minutes | Limit exposure if compromised |
| Scope | Minimum necessary | Principle of least privilege |
| Refresh | Not supported | User re-authenticates for new token |
Transport Security¶
| Requirement | Description |
|---|---|
| HTTPS Required | All server endpoints MUST use HTTPS |
| Localhost Exception | Redirect to localhost MAY use HTTP (browser enforces same-origin) |
| Certificate Validation | Clients MUST validate server certificates |
Rate Limiting¶
Implementations SHOULD apply rate limiting to prevent abuse:
| Endpoint | Recommended Limit |
|---|---|
/api/token/auth | 10 requests per minute per IP |
/api/token/exchange | 20 requests per minute per IP |
Implementation Guide¶
What You Need to Implement¶
- User Authentication
- Integrate with your existing identity provider (OAuth, SAML, LDAP, etc.)
-
Manage user sessions (cookies, tokens, etc.)
-
Access Control
- Define who can obtain tokens (all employees, specific groups, etc.)
-
Implement any approval workflows or interstitials
-
Service Account Management
- Create and manage Google Cloud service accounts for users
- Each user should have their own dedicated service account
-
Service accounts need appropriate IAM roles for Google APIs
-
Token Generation
- Use Google Cloud IAM to generate short-lived access tokens
-
Impersonate user service accounts using your server's credentials
-
Code Storage
- Store authorization codes securely (database, cache, etc.)
- Implement expiration and single-use semantics
Service Account Setup¶
Each user needs a dedicated service account with these roles:
| Role | Purpose |
|---|---|
roles/drive.readonly | Read access to Google Drive (file metadata) |
roles/sheets.editor | Read/write access to Google Sheets |
roles/docs.editor | Read/write access to Google Docs |
roles/slides.editor | Read/write access to Google Slides |
Your server's service account needs:
| Role | Purpose |
|---|---|
roles/iam.serviceAccountAdmin | Create service accounts for users |
roles/iam.serviceAccountTokenCreator | Generate tokens for user service accounts |
Token Generation (Google Cloud)¶
To generate a short-lived token for a user's service account:
from google.auth import impersonated_credentials
from google.oauth2 import service_account
# Your server's credentials
server_credentials = service_account.Credentials.from_service_account_file(
'server-sa-key.json'
)
# Impersonate the user's service account
target_credentials = impersonated_credentials.Credentials(
source_credentials=server_credentials,
target_principal='user-sa@project.iam.gserviceaccount.com',
target_scopes=[
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/documents',
'https://www.googleapis.com/auth/presentations',
'https://www.googleapis.com/auth/drive.readonly',
],
lifetime=3600, # 1 hour
)
# Refresh to get the actual token
target_credentials.refresh(google.auth.transport.requests.Request())
access_token = target_credentials.token
expiry = target_credentials.expiry
Client Implementation¶
The reference client implementation is available at: client/src/extrasuite/client/credentials.py
Key Client Responsibilities¶
- Start a local HTTP server on a random available port
- Open the browser to the server's auth endpoint with the port number
- Wait for the redirect to receive the authorization code
- Exchange the code for a token via POST request
- Cache the token locally with appropriate file permissions (0600)
- Return cached tokens when still valid (with 60-second buffer)
Fallback for Headless Environments¶
If the browser cannot be opened (headless server, SSH session):
- Print the authentication URL to stdout
- Optionally accept the authorization code via stdin
- Display the code on the callback page for manual copy/paste
Compatibility with ExtraSuite Reference Implementation¶
The reference implementation at github.com/think41/extrasuite implements this specification with:
- Google OAuth for user authentication
- Firestore for code storage
- Google Cloud Run for hosting
- Automatic service account provisioning
If you implement this specification, your server will be compatible with:
- The
extrasuiteclient library - All ExtraSuite skills (gsheetx, gslidex, gdocx)
- Any AI agent that uses the ExtraSuite protocol
Example Flows¶
Flow 1: First-Time User¶
1. User: "Read my spreadsheet"
2. Agent invokes skill, which calls CredentialsManager
3. CredentialsManager finds no cached token
4. CredentialsManager starts local server on port 8085
5. CredentialsManager opens browser to:
https://your-server.com/api/token/auth?port=8085
6. Server redirects to your login page
7. User logs in with corporate SSO
8. Server checks user is in "AI Tools" group (your policy)
9. Server generates auth code "abc123"
10. Server redirects browser to:
http://localhost:8085/on-authentication?code=abc123
11. CredentialsManager receives code
12. CredentialsManager POSTs to /api/token/exchange
13. Server returns token + expiry + service account email
14. CredentialsManager caches token, returns to agent
15. Agent uses token to read spreadsheet
Flow 2: Returning User (Valid Session)¶
1. User: "Update the sales report"
2. Agent invokes skill, CredentialsManager finds expired token
3. CredentialsManager starts local server on port 9012
4. CredentialsManager opens browser to:
https://your-server.com/api/token/auth?port=9012
5. Server recognizes user's session cookie (still valid)
6. Server skips login, generates new auth code
7. Server redirects to localhost:9012
8. Browser opens briefly and closes
9. CredentialsManager exchanges code for new token
10. Agent updates spreadsheet
Flow 3: Access Denied¶
1. User: "Read my spreadsheet"
2. Agent invokes skill, CredentialsManager starts auth flow
3. Browser opens to server
4. User logs in successfully
5. Server checks policies: user not in allowed group
6. Server redirects to:
http://localhost:8085/on-authentication?error=access_denied&error_description=...
7. CredentialsManager displays error
8. Agent reports: "Access denied - contact your administrator"
Delegation Protocol (Optional)¶
The delegation protocol enables AI agents to obtain user-level access tokens for APIs like Gmail, Calendar, and Apps Script. Unlike the service account flow above (where tokens act as a service account), delegation tokens act as the user via Google's domain-wide delegation.
This protocol mirrors the service account flow — same localhost redirect pattern, same auth code exchange — but with different endpoints and response format.
Prerequisites¶
- Domain-wide delegation configured in Google Workspace Admin Console
- Server has
DELEGATION_ENABLED=true - Optionally,
DELEGATION_SCOPESrestricts which scopes can be requested
Endpoint 1: Start Delegation Authentication¶
Purpose: Initiate the delegation flow. The server resolves scope names, validates against the optional allowlist, authenticates the user, and redirects to the CLI's local server.
Request¶
| Parameter | Type | Required | Description |
|---|---|---|---|
port | integer | Yes | Port number where the CLI is listening (1024-65535) |
scopes | string | Yes | Comma-separated scope names (e.g., gmail.send,calendar) |
reason | string | No | Human-readable reason for the request (logged for audit) |
Scope names are short identifiers that map to full Google OAuth scope URLs by prefixing https://www.googleapis.com/auth/. For example:
| Short name | Full scope URL |
|---|---|
gmail.send | https://www.googleapis.com/auth/gmail.send |
gmail.readonly | https://www.googleapis.com/auth/gmail.readonly |
calendar | https://www.googleapis.com/auth/calendar |
calendar.readonly | https://www.googleapis.com/auth/calendar.readonly |
drive | https://www.googleapis.com/auth/drive |
drive.readonly | https://www.googleapis.com/auth/drive.readonly |
script.projects | https://www.googleapis.com/auth/script.projects |
Server Behavior¶
The server MUST:
- Check if delegation is enabled — return 404 if not
- Resolve and validate scopes
- Prefix each short name with
https://www.googleapis.com/auth/ - If
DELEGATION_SCOPESallowlist is configured, reject scopes not in the list (400) - Authenticate the user (same as service account flow)
- Generate a delegation authorization code
- Associate it with the user's email, requested scopes, and reason
- Same TTL and single-use requirements as service account auth codes
- Redirect to localhost — same format as service account flow
Responses¶
Success (Redirect):
Error - Delegation Not Enabled:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"detail": "Domain-wide delegation is not enabled on this server"
}
Error - Disallowed Scope:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"detail": "Disallowed scopes: gmail.send"
}
Endpoint 2: Exchange Code for Delegation Token¶
Purpose: Exchange a valid delegation authorization code for a user-level access token.
Request¶
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The delegation authorization code received via localhost redirect |
Server Behavior¶
The server MUST:
- Validate the authorization code (same requirements as service account flow)
- Log the delegation request for audit (user email, scopes, reason, timestamp)
- Generate a delegated access token
- Build a JWT with
subset to the user's email and requested scopes - Sign the JWT using the server's service account via IAM
signBlobAPI - Exchange the signed JWT at Google's token endpoint for an access token
- If delegation fails (e.g., scope not authorized in Workspace Admin Console), return 403
- Return token information
Response¶
Success:
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "ya29.a0AfH6SMB...",
"expires_at": "2026-01-23T14:30:00Z",
"scopes": [
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/calendar"
]
}
| Field | Type | Description |
|---|---|---|
access_token | string | The access token for Google API calls (acts as the user) |
expires_at | string | ISO 8601 timestamp when the token expires |
scopes | array | List of granted scope URLs |
Error - Invalid Code:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"detail": "Invalid or expired auth code"
}
Error - Delegation Failed:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"detail": "Domain-wide delegation failed. The requested scopes may not be authorized in Google Workspace Admin Console."
}
Delegation Security Requirements¶
All security requirements from the service account flow apply (auth code TTL, single-use, entropy, transport security, rate limiting).
Additionally:
| Requirement | Value | Rationale |
|---|---|---|
| Scope allowlist | Optional (DELEGATION_SCOPES) | Defense-in-depth on top of Workspace Admin Console |
| Audit logging | All requests logged | User, scopes, reason, timestamp recorded |
| Workspace Admin enforcement | Required | Google rejects unauthorized scopes at token generation |
Delegation Example Flow¶
1. User: "Send this report via email"
2. Agent needs gmail.send scope
3. Agent calls CredentialsManager.get_oauth_token(["gmail.send"], reason="sending report")
4. CredentialsManager starts local server on port 8085
5. CredentialsManager opens browser to:
https://your-server.com/api/delegation/auth?port=8085&scopes=gmail.send&reason=sending+report
6. Server validates scopes against DELEGATION_SCOPES allowlist (if configured)
7. Server authenticates user (or uses existing session)
8. Server generates delegation auth code with {email, scopes, reason}
9. Server redirects browser to:
http://localhost:8085/on-authentication?code=abc123
10. CredentialsManager receives code
11. CredentialsManager POSTs to /api/delegation/exchange
12. Server logs delegation request, generates token via domain-wide delegation
13. Server returns {access_token, expires_at, scopes}
14. Agent uses token to send email via Gmail API
Changelog¶
| Version | Date | Changes |
|---|---|---|
| 2.0 | 2026-02-26 | Add v2 session-token protocol (30-day session, headless Phase 2) |
| 1.1 | 2026-02-10 | Add delegation protocol specification |
| 1.0 | 2026-01-23 | Initial specification |
v2 Session Token Protocol¶
The v2 protocol eliminates recurring browser interruptions by splitting authentication into two phases:
- Phase 1 (once per ~30 days): Browser-based OAuth flow → 30-day session token stored locally
- Phase 2 (every command, headless): Session token exchanged for a short-lived access token, no browser required
Overview¶
Phase 1 — Session Establishment (once per 30 days):
┌──────────────┐ ┌──────────────────┐
│ AI Agent │ │ Auth Server │
│ (CLI) │ │ │
│ 1. Start │─────────────────────▶│ 2. Authenticate │
│ local │ GET /api/token/auth │ user via │
│ server │ ?port=N │ Google OAuth │
│ │◀─────────────────────│ │
│ 3. Receive │ localhost:N?code=X │ │
│ code │ │ │
│ 4. Exchange │─────────────────────▶│ 5. Validate │
│ for │ POST /api/auth/ │ code, issue │
│ session │ session/exchange │ 30-day token │
│ 6. Store │◀─────────────────────│ │
│ session │ {session_token, │ │
│ locally │ email, expires_at} │ │
└──────────────┘ └──────────────────┘
Phase 2 — Access Token Exchange (every command, headless):
┌──────────────┐ ┌──────────────────┐
│ AI Agent │ │ Auth Server │
│ (CLI) │ │ │
│ 1. Load │─────────────────────▶│ 2. Validate │
│ session │ POST /api/auth/ │ session, │
│ token │ token │ generate │
│ │ {session_token, │ access token │
│ │ pseudo_scope, │ │
│ │ reason} │ │
│ 3. Use │◀─────────────────────│ │
│ token │ {access_token, │ │
│ │ expires_at} │ │
└──────────────┘ └──────────────────┘
New Endpoints¶
POST /api/auth/session/exchange¶
Exchange a short-lived auth code (from Phase 1 browser flow) for a 30-day session token.
Request:
{
"code": "auth_code_from_redirect",
"device_mac": "0x1a2b3c4d5e6f",
"device_hostname": "my-laptop.local",
"device_os": "Darwin",
"device_platform": "macOS-14.0-arm64-arm-64bit"
}
Response:
{
"session_token": "raw_session_token_string",
"expires_at": "2026-03-28T10:00:00+00:00",
"email": "user@example.com"
}
The session token is stored locally at ~/.config/extrasuite/session.json (permissions 0600).
POST /api/auth/token¶
Exchange a session token for a short-lived access token. No browser required.
Request:
{
"session_token": "raw_session_token",
"pseudo_scope": "sheet.pull",
"reason": "Pulling Google Sheet for data analysis",
"file_hint": "https://docs.google.com/spreadsheets/d/..."
}
Response:
Pseudo-Scope Table¶
Pseudo-scopes are short names that map to credential type and Google OAuth scope:
| Pseudo-scope | Credential type | Description |
|---|---|---|
sheet.pull | SA | Read Google Sheets |
sheet.push | SA | Write Google Sheets |
doc.pull | SA | Read Google Docs |
doc.push | SA | Write Google Docs |
slide.pull | SA | Read Google Slides |
slide.push | SA | Write Google Slides |
form.pull | SA | Read Google Forms |
form.push | SA | Write Google Forms |
drive.file | SA | Drive file access |
calendar | DWD | Google Calendar |
gmail.compose | DWD | Gmail compose/drafts |
gmail.send | DWD | Gmail send |
gmail.readonly | DWD | Gmail read |
script.projects | DWD | Google Apps Script |
drive | DWD | Full Drive access |
SA = Service Account impersonation, DWD = Domain-Wide Delegation
Admin Session Management Endpoints¶
These endpoints use Bearer session token authentication (Authorization: Bearer <session_token>).
| Endpoint | Description | Auth |
|---|---|---|
GET /api/admin/sessions?email=<email> | List sessions | Self or admin |
DELETE /api/admin/sessions/<hash> | Revoke session | Own session or admin |
POST /api/admin/sessions/revoke-all?email=<email> | Revoke all sessions | Self or admin |
Admin emails are configured via ADMIN_EMAILS env var (CSV).
Session Token Storage¶
Client stores session token at ~/.config/extrasuite/session.json with format:
File permissions: directory 0700, file 0600.
Access Log Format¶
Every call to POST /api/auth/token is logged in Firestore access_logs collection:
| Field | Description |
|---|---|
email | User's email |
session_hash_prefix | First 16 chars of SHA-256(session_token) |
pseudo_scope | Requested pseudo-scope |
credential_type | "sa" or "dwd" |
reason | Caller-provided reason string |
ip | Client IP address |
file_hint | Optional Drive URL/ID |
timestamp | Request time |
expires_at | 30-day TTL for auto-cleanup |
Device Fingerprint Fields¶
Collected at Phase 1 session issuance and stored server-side for audit:
| Field | Source |
|---|---|
device_mac | uuid.getnode() as hex |
device_hostname | socket.gethostname() |
device_os | platform.system() |
device_platform | platform.platform() |