# Kimi CLI OAuth Authentication Flow: Device-Code Login, Keyring Migration, and Token Refresh

> Learn the Kimi CLI OAuth authentication flow using device-code login. Discover how it securely stores tokens, migrates keyrings, and manages token refresh for seamless access.

- Repository: [Moonshot AI/kimi-cli](https://github.com/MoonshotAI/kimi-cli)
- Tags: how-to-guide
- Published: 2026-07-21

---

**Kimi CLI authenticates to the Kimi Code platform through an OAuth 2.0 device-code grant, stores tokens in atomic local JSON files, and automatically migrates legacy keyring entries while refreshing access tokens in the background.**

The Kimi CLI project (`MoonshotAI/kimi-cli`) implements its entire authentication lifecycle in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py). Understanding the Kimi CLI OAuth authentication flow is essential for anyone extending the client or debugging login issues, because the module handles everything from device-code polling to cross-process token refresh locks.

## How the Kimi CLI Device-Code OAuth Flow Works

The implementation follows the standard OAuth 2.0 device authorization grant. The `OAuthManager` orchestrates the process at runtime, splitting work between three stages: device-code acquisition, user verification, and token retrieval.

### Device-Code Acquisition

The `request_device_authorization` coroutine initiates the flow by posting to the Kimi OAuth server:

```python
async def request_device_authorization() -> DeviceAuthorization:
    async with (
        new_client_session() as session,
        session.post(
            f"{_oauth_host().rstrip('/')}/api/oauth/device_authorization",
            data={"client_id": KIMI_CODE_CLIENT_ID},
            headers=_common_headers(),
        ) as response,
    ):
        data = await response.json(content_type=None)
        status = response.status
    if status != 200:
        raise OAuthError(f"Device authorization failed: {data}")
    return DeviceAuthorization(
        user_code=str(data["user_code"]),
        device_code=str(data["device_code"]),
        verification_uri=str(data.get("verification_uri") or ""),
        verification_uri_complete=str(data["verification_uri_complete"]),
        expires_in=int(data.get("expires_in") or 0) or None,
        interval=int(data.get("interval") or 5),
    )

```

This function returns a `DeviceAuthorization` dataclass containing the `user_code`, `device_code`, and verification URLs required for the next step. In [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py), the CLI builds the POST request to `api/oauth/device_authorization` and validates the HTTP status before parsing the response.

### User Verification and Token Polling

The high-level `login_kimi_code` coroutine drives user interaction. It first calls `request_device_authorization`, yields a verification URL event for the UI layer, and then enters a polling loop:

```python
auth = await request_device_authorization()

# ... yield verification URL event ...

while True:
    status, data = await _request_device_token(auth)
    if status == 200 and "access_token" in data:
        token = OAuthToken.from_response(data)
        break
    # handle errors, wait `auth.interval` seconds, then retry

```

If `open_browser` is enabled, the CLI opens the verification URI automatically via `webbrowser.open`. The `_request_device_token` helper repeatedly polls the `/api/oauth/token` endpoint using the `device_code` until the user authorizes the request or the code expires.

## Token Persistence: File Storage and Keyring Migration

Once the server returns an access token, the CLI must persist it securely. The codebase in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py) now prefers file-based storage but retains keyring support for backward compatibility.

### The OAuthToken Model

The token itself is defined as a slots-based dataclass:

```python
@dataclass(slots=True)
class OAuthToken:
    access_token: str
    refresh_token: str
    expires_at: float
    scope: str
    token_type: str
    expires_in: float = 0.0

```

`OAuthToken.from_response` reconstructs the object from the server's JSON payload, and instances can be serialized to plain dictionaries for storage.

### File-Based Token Storage

File helpers such as `_credentials_dir`, `_credentials_path`, and `_credentials_lock_path` resolve a per-key JSON file under the shared user directory (typically `~/.kimi/credentials`). The `_save_to_file` function writes atomically using a temporary file followed by `os.replace`, preventing corruption if the process crashes. Conversely, `_load_from_file` reads the JSON and instantiates an `OAuthToken`.

### Keyring Fallback and Automatic Migration

For legacy installations, `_load_from_keyring` reads a password entry from the OS keyring:

```python
def _load_from_keyring(key: str) -> OAuthToken | None:
    raw = keyring.get_password(KEYRING_SERVICE, key)
    # ... JSON-decode and return OAuthToken ...

```

When `OAuthRef.storage == "keyring"`, the `load_tokens` function first checks the file cache, then falls back to keyring. On a successful keyring load, the token is immediately migrated to a file via `_save_to_file` and the keyring entry is deleted. This ensures old credentials move to the modern file-based backend without user intervention.

### Public API for Saving Tokens

The `save_tokens` function enforces the migration at write time:

```python
def save_tokens(ref: OAuthRef, token: OAuthToken) -> OAuthRef:
    if ref.storage == "keyring":
        logger.warning("Keyring storage is deprecated; saving OAuth tokens to file.")
        ref = OAuthRef(storage="file", key=ref.key)
    _save_to_file(ref.key, token)
    return ref

```

Any attempt to save to keyring is intercepted and redirected to the file backend, returning an updated `OAuthRef` so that subsequent reads use the correct path.

## Runtime Token Management with OAuthManager

`OAuthManager` is instantiated once per CLI process or web server and centralizes all token operations. According to the source code in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py), it handles loading credentials, migrating legacy storage, caching access tokens, and refreshing them proactively.

### Loading and Migrating Legacy Credentials

On initialization, the manager iterates over OAuth references in the global `Config` using `_iter_oauth_refs`. It then runs `_migrate_oauth_storage` to convert any remaining keyring entries to files before the rest of the application needs them.

### Background Token Refresh

The `refreshing` method returns an async context manager that spawns a background task. This task sleeps for `REFRESH_INTERVAL_SECONDS` (60 seconds) and calls `ensure_fresh` each cycle. If the process wakes after a long pause (elapsed time greater than twice the interval), the manager forces an immediate refresh. Successful refreshes update the in-memory cache and the live LLM client via `_apply_access_token`.

### Cross-Process Lock for Refresh Safety

Token refresh is guarded by `_CrossProcessLock`. The implementation uses `fcntl.flock` on Unix and `msvcrt.locking` on Windows, ensuring that only a single process performs a refresh even when multiple CLI instances or workers run concurrently.

### The Refresh Algorithm

Inside `_refresh_tokens`, the manager performs the following steps:

1. Load the persisted token using `load_tokens`.
2. Check if the token is close to expiry against `_refresh_threshold`.
3. Call `refresh_token` to exchange the refresh token for a new pair.
4. On HTTP 401 or 403 (`OAuthUnauthorized`), mark the refresh token as rejected via `_mark_refresh_token_rejected`.
5. Write the new token back with `save_tokens` and update the in-memory cache.

This algorithm guarantees that stale tokens are detected and renewed before they expire.

## Practical Kimi CLI OAuth Usage Examples

### Logging in via the CLI

The simplest entry point is the built-in login command:

```bash
kimi login

```

This command is exposed in [`src/kimi_cli/cli/info.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/cli/info.py) and ultimately invokes `login_kimi_code`. After the user visits the verification URL and authorizes the device, the new token is saved to `~/.kimi/credentials/kimi-code.json`.

### Resolving API Keys in Code

Applications integrating Kimi CLI can query the current access token programmatically:

```python
from kimi_cli.auth.oauth import OAuthManager, OAuthRef
from kimi_cli.config import load_config

cfg = load_config()
manager = OAuthManager(cfg)

# Resolve the API key for a provider that uses OAuth

api_key = manager.resolve_api_key(
    cfg.providers["kimi"].api_key,
    cfg.providers["kimi"].oauth,
)
print("Current access token:", api_key)

```

`resolve_api_key` returns the cached access token when available; otherwise, it falls back to a static API key. This makes the OAuth flow transparent to downstream API consumers.

### Background Refresh in Long-Running Processes

For servers or long-running scripts, wrap the runtime in the `refreshing` context manager:

```python
async with oauth_manager.refreshing(runtime):
    # handle requests; tokens stay fresh automatically

    await serve_requests()

```

This pattern keeps the access token valid indefinitely without blocking request handling.

## Summary

- **Kimi CLI OAuth authentication flow** uses an OAuth 2.0 device-code grant implemented in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py).
- The `request_device_authorization` function acquires a device code, while `login_kimi_code` handles user verification and token polling.
- Tokens are persisted as atomic JSON files under `~/.kimi/credentials`, and legacy keyring entries are automatically migrated on first read.
- `OAuthManager` loads credentials, caches access tokens, and refreshes them in the background using a cross-process lock.
- The refresh algorithm detects expiry, exchanges refresh tokens, handles 401/403 rejection, and writes new credentials back to disk.

## Frequently Asked Questions

### How does Kimi CLI handle OAuth token storage securely?

Kimi CLI writes tokens to a local JSON file using an atomic `os.replace` operation via `_save_to_file` in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py). Earlier versions stored tokens in the OS keyring, but the current codebase treats keyring as deprecated and automatically migrates those entries to files on load.

### What happens if the access token expires while the CLI is running?

The `OAuthManager` spawns a background refresh task when entered through the `refreshing` async context manager. It checks token age at regular intervals and invokes `_refresh_tokens` before the token expires. If the refresh fails with an unauthorized error, the token is marked rejected and the user must log in again.

### Can multiple Kimi CLI processes refresh the same token simultaneously?

No. The `_CrossProcessLock` class in [`src/kimi_cli/auth/oauth.py`](https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py) uses `fcntl.flock` on Unix and `msvcrt.locking` on Windows to serialize refresh attempts. Only one process can perform the token refresh; others wait until the lock is released and then read the updated file.

### Why does Kimi CLI use the OAuth device-code flow instead of a browser redirect?

The device-code flow is ideal for CLI applications because the client runs in a terminal without a local HTTP listener. The user completes authorization on a separate device or browser window by visiting the verification URI, while the CLI polls `_request_device_token` in the background until the server issues the access and refresh tokens.