Add device authorization to app-server gateway OAuth (#60486)

## What changed

- Advertise `supportsDeviceAuth` in `account/gatewayOAuth/read` and accept `deviceAuth: true` in `account/gatewayOAuth/login`.
- Return a device prompt with `verificationUrl`, `userCode`, and `loginId` before approval, then notify the initiating connection of success or failure with the matching `loginId`.
- Reuse or refresh saved credentials when possible, returning `prompt: null` without another device authorization. Update protocol schemas, generated bindings, and documentation.

## Testing

Add an integration test covering the device prompt, correlated completion notification, capability reporting, and token refresh without another device grant.

GitOrigin-RevId: 6ecc38c9baadcfda3272654ed71e4aaa431421bf
This commit is contained in:
smurching-oai
2026-11-10 01:28:45 +00:00
committed by copyberry
parent 1885417cd4
commit d8c42ecf8f
23 changed files with 414 additions and 26 deletions
@@ -1885,16 +1885,26 @@ class GatewayOAuthLoginParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
device_auth: Annotated[
bool | None,
Field(
alias="deviceAuth",
description="Select device authorization when the server advertises support for it.",
),
] = None
model_provider: Annotated[
str | None,
Field(alias="modelProvider", description="Defaults to the current provider when omitted."),
] = None
class GatewayOAuthLoginResponse(BaseModel):
class GatewayOAuthPrompt(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
login_id: Annotated[str, Field(alias="loginId")]
user_code: Annotated[str, Field(alias="userCode")]
verification_url: Annotated[str, Field(alias="verificationUrl")]
class GatewayOAuthReadParams(BaseModel):
@@ -8945,10 +8955,21 @@ class GatewayOAuthChangedNotification(BaseModel):
),
] = None
error: str | None = None
login_id: Annotated[
str | None,
Field(alias="loginId", description="Identifies the completed device login attempt."),
] = None
provider_id: Annotated[str, Field(alias="providerId")]
status: GatewayOAuthStatus
class GatewayOAuthLoginResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
prompt: GatewayOAuthPrompt | None = None
class GatewayOAuthReadResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -8966,6 +8987,13 @@ class GatewayOAuthReadResponse(BaseModel):
GatewayOAuthStatus | None,
Field(description="Null when the effective provider does not use gateway OAuth."),
] = None
supports_device_auth: Annotated[
bool | None,
Field(
alias="supportsDeviceAuth",
description="Whether the administrator configured a device authorization endpoint.",
),
] = False
class GetAccountResponse(BaseModel):