fix: tighten oauth and source resource handling

This commit is contained in:
lda
2026-06-14 00:53:42 +07:00 Verified
parent 2609679b87
commit d596898b96
19 changed files with 228 additions and 21 deletions
@@ -0,0 +1,836 @@
# OAuth Login Auth Records Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a local/dev OAuth login command that uses provider profiles to create typed `oauth_refresh_token` auth records for sources such as Google Drive MCP.
**Architecture:** OAuth login is admin/CLI setup, not runtime execution. Provider profiles describe how to start authorization-code + PKCE login. The resulting refresh token is saved as a typed auth record. Runtime source calls continue to consume auth records through source-owned binders from the previous slice.
**Tech Stack:** Python 3.14, Typer, Authlib (new dependency if accepted), pytest, basedpyright, ruff, local callback or pasted redirect URL flow.
---
## Dependency On Previous Slice
This plan assumes `docs/superpowers/plans/2026-06-13-typed-auth-records-and-mcp-binder.md` is complete:
- `wf_api.auth.StoredAuthRecord`
- `wf_api.auth.OAuthRefreshTokenAuth`
- auth store can save typed records
- MCP runtime can use typed records through `McpAuthBinder`
Before starting provider-profile/login work, verify that an `oauth_refresh_token`
record loaded from the auth store reaches `McpAuthBinder` as
`OAuthRefreshTokenAuth`. The first slice currently preserves some legacy
`load_auth()` compatibility, and that bridge must not downgrade
`oauth_refresh_token` into `OpaqueAuth`.
Do not implement this plan first.
## File Structure
- Modify `src/wf_config/models.py`: add OAuth provider profile config models under a top-level auth config section.
- Modify `src/wf_config/loader.py` only if config-relative loading needs provider profile defaults.
- Create `src/wf_cli/oauth.py`: OAuth login flow helpers and provider profile DTOs for CLI use.
- Modify `src/wf_cli/commands/auth_admin.py`: add `oauth-login` command.
- Modify `src/wf_cli/context.py` only if CLI context must expose loaded workflow config auth providers.
- Add tests:
- `tests/wf_config/test_config_models.py`
- `tests/wf_cli/test_auth_oauth_login.py`
- Docs:
- `docs/wf_cli.md`
- `docs/superpowers/specs/2026-06-06-auth-source-secrets-boundary.md`
- `docs/current_roadmap.md`
## Task 0: OAuth Runtime Bridge Regression
**Files:**
- Modify: `src/wf_api/auth.py`
- Test: `tests/wf_api/test_auth.py`
- Test: `tests/wf_sources_mcp/test_auth_storage_exports.py`
- [ ] **Step 1: Add failing compatibility parser test**
Append to `tests/wf_api/test_auth.py`:
```python
from pydantic import AnyUrl
from wf_api.auth import OAuthRefreshTokenAuth
def test_auth_record_from_compat_maps_oauth_refresh_token() -> None:
record = auth_record_from_compat(
id="google.drive.personal",
scheme="oauth_refresh_token",
payload={
"client_id": "client",
"client_secret": "secret",
"refresh_token": "refresh",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/drive.readonly"],
},
metadata={"provider": "google"},
)
assert isinstance(record.auth, OAuthRefreshTokenAuth)
assert record.auth.client_id == "client"
assert str(record.auth.token_url) == "https://oauth2.googleapis.com/token"
assert record.auth.scopes == ("https://www.googleapis.com/auth/drive.readonly",)
```
- [ ] **Step 2: Add failing store/runtime round-trip test**
Append to `tests/wf_sources_mcp/test_auth_storage_exports.py`:
```python
from wf_api.auth import OAuthRefreshTokenAuth, auth_record_from_compat
def test_file_auth_store_oauth_refresh_token_survives_legacy_load_auth_bridge(
tmp_path: Path,
) -> None:
store = FileAuthStore(tmp_path)
record = StoredAuthRecord(
id="google.drive.personal",
auth=OAuthRefreshTokenAuth(
client_id="client",
client_secret="secret",
refresh_token="refresh",
token_url="https://oauth2.googleapis.com/token",
scopes=("https://www.googleapis.com/auth/drive.readonly",),
),
)
store.save_auth_record(record)
legacy = store.load_auth("google.drive.personal")
assert legacy is not None
restored = auth_record_from_compat(
id=legacy.connection_id,
scheme=legacy.scheme,
payload=legacy.payload,
metadata={},
)
assert isinstance(restored.auth, OAuthRefreshTokenAuth)
assert restored.auth.refresh_token == "refresh"
```
- [ ] **Step 3: Run tests and verify failure**
Run:
```bash
uv run pytest tests/wf_api/test_auth.py::test_auth_record_from_compat_maps_oauth_refresh_token tests/wf_sources_mcp/test_auth_storage_exports.py::test_file_auth_store_oauth_refresh_token_survives_legacy_load_auth_bridge -q
```
Expected: fails because `auth_record_from_compat()` currently falls through to
`OpaqueAuth` for `scheme="oauth_refresh_token"`.
- [ ] **Step 4: Implement explicit OAuth compatibility parsing**
In `src/wf_api/auth.py`, add this `match` arm before `case _` in
`auth_record_from_compat()`:
```python
case "oauth_refresh_token":
client_id = payload_dict.get("client_id")
client_secret = payload_dict.get("client_secret")
refresh_token = payload_dict.get("refresh_token")
token_url = payload_dict.get("token_url")
raw_scopes = payload_dict.get("scopes", ())
scopes = (
tuple(str(scope) for scope in raw_scopes)
if isinstance(raw_scopes, list | tuple)
else ()
)
if not isinstance(client_id, str) or not client_id:
raise ValueError("oauth_refresh_token client_id is required")
if not isinstance(client_secret, str):
raise ValueError("oauth_refresh_token client_secret is required")
if not isinstance(refresh_token, str) or not refresh_token:
raise ValueError("oauth_refresh_token refresh_token is required")
if not isinstance(token_url, str) or not token_url:
raise ValueError("oauth_refresh_token token_url is required")
auth = OAuthRefreshTokenAuth(
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
token_url=token_url,
scopes=scopes,
)
```
- [ ] **Step 5: Run focused verification**
Run:
```bash
uv run pytest tests/wf_api/test_auth.py tests/wf_sources_mcp/test_auth_storage_exports.py tests/wf_sources_mcp/test_auth.py -q
uv run basedpyright --level error src/wf_api/auth.py tests/wf_api/test_auth.py tests/wf_sources_mcp/test_auth_storage_exports.py
uv run ruff check src/wf_api/auth.py tests/wf_api/test_auth.py tests/wf_sources_mcp/test_auth_storage_exports.py
```
Expected: all pass.
- [ ] **Step 6: Commit**
```bash
git add src/wf_api/auth.py tests/wf_api/test_auth.py tests/wf_sources_mcp/test_auth_storage_exports.py
git commit -m "fix: preserve oauth refresh auth through runtime bridge"
```
## Task 1: OAuth Provider Profile Config
**Files:**
- Modify: `src/wf_config/models.py`
- Modify: `src/wf_config/__init__.py`
- Test: `tests/wf_config/test_config_models.py`
- [ ] **Step 1: Add failing config model test**
Append:
```python
from wf_config import WorkflowConfigFile
def test_workflow_config_parses_oauth_provider_profile() -> None:
config = WorkflowConfigFile.model_validate(
{
"auth": {
"providers": {
"google": {
"kind": "oauth_authorization_code_pkce",
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
"client_secret_env": "GOOGLE_OAUTH_CLIENT_SECRET",
"scopes": [
"https://www.googleapis.com/auth/drive.readonly",
],
}
}
}
}
)
provider = config.auth.providers["google"]
assert provider.kind == "oauth_authorization_code_pkce"
assert provider.client_id_env == "GOOGLE_OAUTH_CLIENT_ID"
assert provider.scopes == ("https://www.googleapis.com/auth/drive.readonly",)
```
- [ ] **Step 2: Run test and verify failure**
Run:
```bash
uv run pytest tests/wf_config/test_config_models.py::test_workflow_config_parses_oauth_provider_profile -q
```
Expected: fails because `auth` config does not exist.
- [ ] **Step 3: Implement config models**
In `src/wf_config/models.py`, add:
```python
class OAuthProviderConfig(BaseModel):
kind: Literal["oauth_authorization_code_pkce"]
auth_url: AnyUrl
token_url: AnyUrl
client_id_env: str
client_secret_env: str | None = None
scopes: tuple[str, ...] = ()
redirect_uri: str = "http://127.0.0.1:0/oauth/callback"
class AuthConfig(BaseModel):
providers: dict[str, OAuthProviderConfig] = Field(default_factory=dict)
```
Add `auth: AuthConfig = Field(default_factory=AuthConfig)` to `WorkflowConfigFile`.
Export `AuthConfig` and `OAuthProviderConfig` in `src/wf_config/__init__.py`.
- [ ] **Step 4: Run config tests**
Run:
```bash
uv run pytest tests/wf_config/test_config_models.py -q
uv run basedpyright --level error src/wf_config tests/wf_config/test_config_models.py
uv run ruff check src/wf_config tests/wf_config/test_config_models.py
```
Expected: all pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_config/models.py src/wf_config/__init__.py tests/wf_config/test_config_models.py
git commit -m "feat: add oauth provider config"
```
## Task 2: OAuth Login Flow Helper
**Files:**
- Create: `src/wf_cli/oauth.py`
- Test: `tests/wf_cli/test_auth_oauth_login.py`
- [ ] **Step 1: Add failing helper tests**
Create `tests/wf_cli/test_auth_oauth_login.py`:
```python
from __future__ import annotations
import pytest
from wf_cli.oauth import OAuthLoginResult, build_oauth_record
from wf_config import OAuthProviderConfig
from wf_api.auth import OAuthRefreshTokenAuth
def test_build_oauth_record_creates_refresh_token_auth() -> None:
provider = OAuthProviderConfig(
kind="oauth_authorization_code_pkce",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
client_id_env="GOOGLE_OAUTH_CLIENT_ID",
client_secret_env="GOOGLE_OAUTH_CLIENT_SECRET",
scopes=("https://www.googleapis.com/auth/drive.readonly",),
)
result = OAuthLoginResult(
refresh_token="refresh",
subject="[email protected]",
scopes=("https://www.googleapis.com/auth/drive.readonly",),
)
record = build_oauth_record(
auth_ref="google.drive.personal",
provider_name="google",
provider=provider,
client_id="client",
client_secret="secret",
result=result,
)
assert record.id == "google.drive.personal"
assert isinstance(record.auth, OAuthRefreshTokenAuth)
assert record.auth.client_id == "client"
assert record.auth.client_secret == "secret"
assert record.auth.refresh_token == "refresh"
assert record.metadata["provider"] == "google"
assert record.metadata["subject"] == "[email protected]"
def test_build_oauth_record_rejects_missing_refresh_token() -> None:
provider = OAuthProviderConfig(
kind="oauth_authorization_code_pkce",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
client_id_env="GOOGLE_OAUTH_CLIENT_ID",
)
with pytest.raises(ValueError, match="refresh token"):
build_oauth_record(
auth_ref="google.drive.personal",
provider_name="google",
provider=provider,
client_id="client",
client_secret=None,
result=OAuthLoginResult(refresh_token=None),
)
```
- [ ] **Step 2: Run tests and verify failure**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py -q
```
Expected: fails because `wf_cli.oauth` does not exist.
- [ ] **Step 3: Implement minimal helper**
Create `src/wf_cli/oauth.py`:
```python
from __future__ import annotations
from dataclasses import dataclass
from wf_api.auth import OAuthRefreshTokenAuth, StoredAuthRecord
from wf_config import OAuthProviderConfig
@dataclass(frozen=True, slots=True)
class OAuthLoginResult:
refresh_token: str | None
subject: str | None = None
scopes: tuple[str, ...] = ()
def build_oauth_record(
*,
auth_ref: str,
provider_name: str,
provider: OAuthProviderConfig,
client_id: str,
client_secret: str | None,
result: OAuthLoginResult,
) -> StoredAuthRecord:
if not result.refresh_token:
raise ValueError("OAuth login did not return a refresh token")
metadata: dict[str, object] = {"provider": provider_name}
if result.subject:
metadata["subject"] = result.subject
if result.scopes:
metadata["scopes"] = list(result.scopes)
return StoredAuthRecord(
id=auth_ref,
auth=OAuthRefreshTokenAuth(
client_id=client_id,
client_secret=client_secret or "",
refresh_token=result.refresh_token,
token_url=provider.token_url,
scopes=tuple(result.scopes or provider.scopes),
),
metadata=metadata,
)
```
- [ ] **Step 4: Run helper tests**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py -q
uv run basedpyright --level error src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
uv run ruff check src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
```
Expected: all pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
git commit -m "feat: build oauth auth records"
```
## Task 3: Interactive OAuth Exchange Abstraction
**Files:**
- Modify: `src/wf_cli/oauth.py`
- Test: `tests/wf_cli/test_auth_oauth_login.py`
- [ ] **Step 1: Add failing test with fake OAuth client**
Append:
```python
from wf_cli.oauth import OAuthCodeLoginFlow
class _FakeOAuthClient:
def __init__(self) -> None:
self.authorization_url = "https://auth.example/authorize?state=abc"
self.fetch_calls: list[str] = []
def create_authorization_url(self, auth_url: str, **kwargs: object) -> tuple[str, str]:
assert auth_url == "https://accounts.google.com/o/oauth2/v2/auth"
return self.authorization_url, "state-123"
async def fetch_token(self, token_url: str, authorization_response: str) -> dict[str, object]:
self.fetch_calls.append(authorization_response)
return {
"refresh_token": "refresh",
"scope": "https://www.googleapis.com/auth/drive.readonly",
}
async def test_oauth_code_login_flow_uses_injected_client() -> None:
provider = OAuthProviderConfig(
kind="oauth_authorization_code_pkce",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
client_id_env="GOOGLE_OAUTH_CLIENT_ID",
scopes=("https://www.googleapis.com/auth/drive.readonly",),
)
client = _FakeOAuthClient()
flow = OAuthCodeLoginFlow(client_factory=lambda **kwargs: client)
result = await flow.login_with_authorization_response(
provider=provider,
client_id="client",
client_secret=None,
authorization_response="http://127.0.0.1/callback?code=abc&state=state-123",
)
assert result.refresh_token == "refresh"
assert result.scopes == ("https://www.googleapis.com/auth/drive.readonly",)
assert client.fetch_calls == ["http://127.0.0.1/callback?code=abc&state=state-123"]
```
- [ ] **Step 2: Run test and verify failure**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py::test_oauth_code_login_flow_uses_injected_client -q
```
Expected: fails because `OAuthCodeLoginFlow` does not exist.
- [ ] **Step 3: Implement injectable OAuth flow**
In `src/wf_cli/oauth.py`, add:
```python
from collections.abc import Callable
from typing import Any, Protocol
class OAuthClientLike(Protocol):
def create_authorization_url(self, auth_url: str, **kwargs: object) -> tuple[str, str]: ...
async def fetch_token(self, token_url: str, authorization_response: str) -> dict[str, object]: ...
OAuthClientFactory = Callable[..., OAuthClientLike]
class OAuthCodeLoginFlow:
def __init__(self, client_factory: OAuthClientFactory) -> None:
self._client_factory = client_factory
async def login_with_authorization_response(
self,
*,
provider: OAuthProviderConfig,
client_id: str,
client_secret: str | None,
authorization_response: str | None,
authorization_url_callback: Callable[[str, str], str | None] | None = None,
) -> OAuthLoginResult:
client = self._client_factory(
client_id=client_id,
client_secret=client_secret,
scope=" ".join(provider.scopes),
code_challenge_method="S256",
)
authorization_url, state = client.create_authorization_url(
str(provider.auth_url),
redirect_uri=provider.redirect_uri,
**provider.extra_authorize_params,
)
if authorization_url_callback is not None:
callback_response = authorization_url_callback(authorization_url, state)
if authorization_response is None:
authorization_response = callback_response
if authorization_response is None:
raise ValueError("OAuth authorization response is required")
token = await client.fetch_token(
str(provider.token_url),
authorization_response=authorization_response,
)
refresh_token = token.get("refresh_token")
if refresh_token is not None and not isinstance(refresh_token, str):
raise ValueError("OAuth refresh_token must be a string")
raw_scope = token.get("scope")
scopes = tuple(str(raw_scope).split()) if raw_scope else provider.scopes
return OAuthLoginResult(refresh_token=refresh_token, scopes=scopes)
```
This helper supports pasted authorization response first. Browser callback can be a later refinement.
`authorization_url_callback` is used by the CLI to show the generated URL and
collect the pasted redirected callback URL; tests can inject it to avoid prompt
I/O. Provider-specific authorization parameters such as Google's
`access_type=offline` and `prompt=consent` belong in
`provider.extra_authorize_params`, not in this generic helper.
- [ ] **Step 4: Run helper tests**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py -q
uv run basedpyright --level error src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
uv run ruff check src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
```
Expected: all pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
git commit -m "feat: add oauth code login helper"
```
## Task 4: CLI Command `wf admin auth oauth-login`
**Files:**
- Modify: `src/wf_cli/commands/auth_admin.py`
- Test: `tests/wf_cli/test_auth_oauth_login.py`
- [ ] **Step 1: Add failing CLI test with fake flow**
Append:
```python
from typer.testing import CliRunner
from wf_cli.app import app
def test_auth_oauth_login_saves_record_from_provider_profile(monkeypatch, tmp_path) -> None:
saved: list[object] = []
class _FakeAdmin:
async def save_auth_record(self, **kwargs: object) -> dict[str, object]:
saved.append(kwargs)
return {"id": kwargs["auth_ref"], "scheme": "oauth_refresh_token"}
class _FakeContext:
admin = _FakeAdmin()
async def _fake_login(*args: object, **kwargs: object) -> OAuthLoginResult:
return OAuthLoginResult(refresh_token="refresh")
monkeypatch.setattr(
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
lambda ctx: _FakeContext(),
)
monkeypatch.setattr(
"wf_cli.commands.auth_admin._login_with_pasted_response",
_fake_login,
)
config_path = tmp_path / "wf.config.json"
config_path.write_text(
json.dumps(
{
"auth": {
"providers": {
"google": {
"kind": "oauth_authorization_code_pkce",
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
"scopes": ["https://www.googleapis.com/auth/drive.readonly"],
}
}
}
}
),
encoding="utf-8",
)
monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_ID", "client")
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"admin",
"auth",
"oauth-login",
"google",
"--id",
"google.drive.personal",
"--authorization-response",
"http://127.0.0.1/callback?code=abc&state=state",
],
)
assert result.exit_code == 0, result.output
assert saved
assert saved[0]["auth_ref"] == "google.drive.personal"
assert saved[0]["scheme"] == "oauth_refresh_token"
```
- [ ] **Step 2: Run test and verify failure**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py::test_auth_oauth_login_saves_record_from_provider_profile -q
```
Expected: fails because command does not exist.
- [ ] **Step 3: Implement CLI command**
In `src/wf_cli/commands/auth_admin.py`, add command `oauth-login`.
Implementation outline:
```python
@app.command("oauth-login")
def oauth_login(
ctx: typer.Context,
provider_name: Annotated[str, typer.Argument(help="Auth provider profile name.")],
auth_ref: Annotated[str, typer.Option("--id", help="Auth record id/ref to save.")],
authorization_response: Annotated[
str,
typer.Option("--authorization-response", help="Full redirected callback URL after login."),
],
) -> None:
workflow_config = load_workflow_config_from_typer(ctx)
provider = workflow_config.auth.providers.get(provider_name)
if provider is None:
raise typer.BadParameter(f"unknown auth provider {provider_name!r}")
client_id = os.environ.get(provider.client_id_env)
if not client_id:
raise typer.BadParameter(f"missing env var {provider.client_id_env}")
client_secret = (
os.environ.get(provider.client_secret_env)
if provider.client_secret_env is not None
else None
)
result = run_cli_operation(
load_cli_context_from_typer(ctx),
_login_with_pasted_response(
provider=provider,
client_id=client_id,
client_secret=client_secret,
authorization_response=authorization_response,
),
)
record = build_oauth_record(...)
context = load_cli_context_from_typer(ctx)
saved = run_cli_operation(
context,
context.admin.save_auth_record(
auth_ref=record.id,
scheme="oauth_refresh_token",
payload=record.auth.model_dump(mode="json", exclude={"kind"}),
metadata=record.metadata,
),
)
emit_json(saved)
```
Use existing config loading helpers from `wf_cli.context` if available. If not available, add a small helper in this command module that reads `ctx.params["config"]` or the existing config option path used by `load_cli_context_from_typer`.
Add `_login_with_pasted_response()` wrapper so tests can monkeypatch it:
```python
async def _login_with_pasted_response(...) -> OAuthLoginResult:
from authlib.integrations.httpx_client import AsyncOAuth2Client
flow = OAuthCodeLoginFlow(client_factory=AsyncOAuth2Client)
return await flow.login_with_authorization_response(...)
```
If `authlib` is not yet a dependency, add it with `uv add authlib` in a separate commit or update `pyproject.toml` manually according to project style.
- [ ] **Step 4: Run CLI tests**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py tests/wf_cli/test_auth_admin.py -q
uv run basedpyright --level error src/wf_cli/commands/auth_admin.py src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
uv run ruff check src/wf_cli/commands/auth_admin.py src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py
```
Expected: all pass.
- [ ] **Step 5: Commit**
```bash
git add src/wf_cli/commands/auth_admin.py src/wf_cli/oauth.py tests/wf_cli/test_auth_oauth_login.py pyproject.toml uv.lock
git commit -m "feat: add oauth login auth command"
```
## Task 5: Google Drive MCP Docs And Smoke Instructions
**Files:**
- Modify: `docs/wf_cli.md`
- Modify: `docs/superpowers/specs/2026-06-06-auth-source-secrets-boundary.md`
- Modify: `docs/current_roadmap.md`
- [ ] **Step 1: Document config**
Add a section to `docs/wf_cli.md`:
```md
### Google Drive MCP OAuth Setup
Google Drive MCP is a remote HTTP MCP source:
```json
{
"sources": [
{
"id": "google.drive",
"kind": "mcp",
"transport": {
"kind": "http",
"url": "https://drivemcp.googleapis.com/mcp/v1"
},
"auth_ref": "google.drive.personal"
}
],
"auth": {
"providers": {
"google": {
"kind": "oauth_authorization_code_pkce",
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"client_id_env": "GOOGLE_OAUTH_CLIENT_ID",
"client_secret_env": "GOOGLE_OAUTH_CLIENT_SECRET",
"scopes": [
"https://www.googleapis.com/auth/drive.readonly"
]
}
}
}
}
```
Run OAuth login:
```bash
wf --config wf.config.json admin auth oauth-login google --id google.drive.personal --authorization-response "<redirected URL>"
```
```
Mention that refresh tokens are sensitive and file store is plaintext local/dev only.
- [ ] **Step 2: Update spec/roadmap status**
In the auth spec, mark OAuth login as implemented if this slice is done. In roadmap, update auth line so Google Drive smoke remains optional/manual if credentials are local-only.
- [ ] **Step 3: Run docs-adjacent verification**
Run:
```bash
uv run pytest tests/wf_cli/test_auth_oauth_login.py tests/wf_config/test_config_models.py -q
uv run ruff check src/wf_cli src/wf_config tests/wf_cli/test_auth_oauth_login.py
uv run basedpyright --level error src/wf_cli/oauth.py src/wf_cli/commands/auth_admin.py src/wf_config
git diff --check
```
Expected: tests pass, lint/typecheck clean, no whitespace errors except acceptable CRLF warnings on Windows.
- [ ] **Step 4: Commit**
```bash
git add docs/wf_cli.md docs/superpowers/specs/2026-06-06-auth-source-secrets-boundary.md docs/current_roadmap.md
git commit -m "docs: document oauth auth login"
```
## Self-Review Checklist
- Spec coverage: provider profiles, typed refresh-token records, CLI login, and Drive MCP setup are covered.
- Placeholder scan: no TODO/TBD placeholders.
- Type consistency: plan consistently uses `OAuthProviderConfig`, `OAuthCodeLoginFlow`, `OAuthLoginResult`, and `StoredAuthRecord`.
- Scope check: browser-opening/local callback UX is intentionally not required in this slice; pasted authorization response is enough for a verifiable first pass.