feat: add oauth login auth command

This commit is contained in:
lda
2026-06-13 02:39:04 +07:00 Verified
parent 38c9be850c
commit 17fa743fe2
2 changed files with 150 additions and 1 deletions
+80 -1
View File
@@ -1,14 +1,16 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Annotated
import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.context import config_path_from_context, load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
from wf_cli.oauth import OAuthCodeLoginFlow, OAuthLoginResult, build_oauth_record
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
@@ -140,3 +142,80 @@ def delete_auth_record(
context = load_cli_context_from_typer(ctx)
result = run_cli_operation(context, context.admin.delete_auth_record(auth_ref))
emit_json(result)
async def _login_with_pasted_response(
*,
provider,
client_id: str,
client_secret: str | None,
authorization_response: str,
) -> OAuthLoginResult:
from authlib.integrations.httpx_client import AsyncOAuth2Client
flow = OAuthCodeLoginFlow(client_factory=AsyncOAuth2Client) # type: ignore[arg-type]
return await flow.login_with_authorization_response(
provider=provider,
client_id=client_id,
client_secret=client_secret,
authorization_response=authorization_response,
)
@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:
"""Run an OAuth login flow and save the resulting refresh token as an auth record."""
from wf_config import load_workflow_config
config_path = Path(config_path_from_context(ctx))
config = load_workflow_config(config_path)
provider = 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(
auth_ref=auth_ref,
provider_name=provider_name,
provider=provider,
client_id=client_id,
client_secret=client_secret,
result=result,
)
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)
+70
View File
@@ -1,8 +1,12 @@
from __future__ import annotations
import json
import pytest
from typer.testing import CliRunner
from wf_api.auth import OAuthRefreshTokenAuth
from wf_cli.app import app
from wf_cli.oauth import OAuthCodeLoginFlow, OAuthLoginResult, build_oauth_record
from wf_config import OAuthProviderConfig
@@ -97,3 +101,69 @@ async def test_oauth_code_login_flow_uses_injected_client() -> None:
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"]
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"