Skip to content

Conversation

rakesh-oai
Copy link
Contributor

@rakesh-oai rakesh-oai commented Sep 12, 2025

External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated "Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes.

test

codex-rs % export CODEX_DEVICE_AUTH_BASE_URL=http://localhost:3007
codex-rs % cargo run --bin codex login --experimental_use-device-code
   Compiling codex-login v0.0.0 (/Users/rakesh/code/codex/codex-rs/login)
   Compiling codex-mcp-server v0.0.0 (/Users/rakesh/code/codex/codex-rs/mcp-server)
   Compiling codex-tui v0.0.0 (/Users/rakesh/code/codex/codex-rs/tui)
   Compiling codex-cli v0.0.0 (/Users/rakesh/code/codex/codex-rs/cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.90s
     Running `target/debug/codex login --experimental_use-device-code`
To authenticate, enter this code when prompted: 6Q27-KBVRF with interval 5
^C

The error in the last line is since the poll endpoint is not yet implemented

@rakesh-oai rakesh-oai self-assigned this Sep 12, 2025
Copy link

github-actions bot commented Sep 12, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines 76 to 147
let token_url = format!("{}/deviceauth/token", opts.issuer.trim_end_matches('/'));
loop {
let resp = client
.post(&token_url)
.header("Content-Type", "application/json")
.body(format!(
"{{\"client_id\":\"{}\",\"user_code\":\"{}\"}}",
opts.client_id, uc.user_code
))
.send()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Include device_code parameters when polling token endpoint

The polling loop sends POST {issuer}/deviceauth/token with JSON body {"client_id":…, "user_code":…}. In the OAuth 2.0 device authorization grant, the token request must be form‑encoded and include the device_code returned from the first step together with grant_type=urn:ietf:params:oauth:grant-type:device_code. Without those fields, a compliant issuer will respond with invalid_request and this login flow can never succeed outside the mocked tests (which ignore the request body). Consider sending the device code and grant type per the spec instead of the user-facing code.

Useful? React with 👍 / 👎.

@rakesh-oai
Copy link
Contributor Author

@codex fix comments

@rakesh-oai rakesh-oai marked this pull request as draft September 12, 2025 19:31
Copy link
Contributor

Summary

  • Parsed the device authorization response into explicit user_code, interval, and device_code, fail early when the issuer omits device_code, and poll the token endpoint with a spec-compliant form-encoded body that includes the device grant type. codex-rs/login/src/device_code_auth.rsL66-L92
  • Updated device code login unit-test fixtures so /devicecode/usercode responses now provide the device_code alongside the existing fields. codex-rs/login/src/server.rsL682-L915
  • Adjusted the integration suite’s mock device authorization flows to return device_code values, keeping the tests aligned with the OAuth device code grant requirements. codex-rs/login/tests/suite/device_code_login.rsL47-L303

Testing

  • just fmt
  • just fix -p codex-login
  • cargo test -p codex-login

View task →

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 8402309 to ccb571e Compare September 26, 2025 04:42
@rakesh-oai rakesh-oai marked this pull request as ready for review September 26, 2025 05:43
Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from ccb571e to 7df113e Compare September 26, 2025 06:53
@rakesh-oai
Copy link
Contributor Author

@codex review the PR

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

Comment on lines 73 to 75
// Step 1: request a user code and polling interval
// let usercode_url = format!("{}/devicecode/usercode", opts.issuer.trim_end_matches('/'));
let usercode_url = format!("{}/deviceauth/usercode", "http://localhost:3007");
let mut payload: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect issuer when requesting device codes

The new device-code flow constructs the user-code endpoint with a hard-coded http://localhost:3007 instead of using the ServerOptions.issuer that the caller provides. As a result the CLI will always attempt to contact a local server regardless of configuration, and will fail outright in real deployments (e.g., the default https://auth.openai.com issuer or any overridden URL). The commented-out line shows the intended behavior; the current implementation makes the feature unusable.

Useful? React with 👍 / 👎.

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 7df113e to 375e31a Compare September 26, 2025 07:28
@rakesh-oai
Copy link
Contributor Author

@codex review this PR

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 375e31a to bc835cc Compare September 26, 2025 07:32
Copy link
Contributor

💡 Codex Review

if resp.status().is_success() {
let code_resp: CodeSuccessResp = resp.json().await.map_err(std::io::Error::other)?;
let tokens = exchange_device_code_for_tokens(
&client,
&opts.issuer,
&opts.client_id,
&code_resp.code,
)
.await?;

[P1] Deserialize deviceauth success payload into tokens

When the polling request succeeds, the implementation assumes the body contains a device_code and performs a second /oauth/token exchange. The newly added tests stub /deviceauth/token to return the final id_token/access_token/refresh_token, which matches the typical device-code flow where the poll endpoint itself yields tokens. Deserializing into CodeSuccessResp will fail with missing field device_code and the login will error even after the user has authorized. The success branch should read the token fields directly (or handle both shapes) rather than unconditionally requiring a second exchange.


// Try to parse an error payload; if it's token_pending, sleep and retry
let status = resp.status();
if status == StatusCode::NOT_FOUND {
let elapsed = start.elapsed();
if elapsed >= max_wait {
return Err(std::io::Error::other(
"device auth timed out after 15 minutes",
));
}
let remaining = max_wait - elapsed;
let sleep_for = Duration::from_secs(interval).min(remaining);
tokio::time::sleep(sleep_for).await;
continue;
} else {
return Err(std::io::Error::other(format!(
"device auth failed with status {status}"

[P1] Treat token_pending errors as retryable

Polling only retries when the HTTP status is 404; all other non‑2xx statuses are treated as fatal. The device-code tests model the common behavior of returning 400 with an error payload such as { "error": "token_pending" } while the user is still authorizing. With the current logic a normal pending response will surface device auth failed with status 400 and abort the flow immediately. The loop should parse the error body and continue polling on token_pending (and optionally slow_down) rather than special‑casing 404.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 6de3f1d to ce7bc50 Compare September 26, 2025 16:12
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why id this value coming in different data types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is coming in as string, let me simplify this code.


use crate::server::ServerOptions;

pub(crate) const DEVICE_AUTH_BASE_URL_ENV_VAR: &str = "CODEX_DEVICE_AUTH_BASE_URL";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this change the entire issuer address instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nvm, I missed the part that I introduced commandline option, this is redundent I will delete this.

// Step 1: request a user code and polling interval
let usercode_url = format!("{auth_base_url}/devicecode/usercode");
let mut payload: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
payload.insert(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use strongly typed struct for all request responses?

let start = std::time::Instant::now();

let token_url = format!("{auth_base_url}/deviceauth/token");
loop {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this method can be split up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split the function into multiple smaller functions.

)
.await?;

// Try to exchange for an API key (optional best-effort)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what we are doing for auth callback in regular chatgpt sign up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the old flow we don't use anymore, doesn't matter for device login.

@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 96251bb to 1745620 Compare September 27, 2025 21:36
@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 7d53bf7 to 5425e08 Compare September 28, 2025 17:10
/// EXPERIMENTAL: Use custom OAuth issuer base URL (advanced)
/// Override the OAuth issuer base URL (advanced)
#[arg(long = "experimental_issuer", value_name = "URL", hide = true)]
issuer: Option<String>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late nit:

Suggested change
issuer: Option<String>,
issuer_base_url: Option<String>,

}

#[derive(Serialize)]
struct UserCodeReq<'a> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these lifetimes? Can we go with Strings as properties and simplify.

nit: consider Request/Response suffixes.


#[derive(Deserialize)]
struct CodeSuccessResp {
#[serde(alias = "device_code")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why alias, can the field be device_code ?

// ANSI escape code for bright yellow
const YELLOW: &str = "\x1b[93m";
const RESET: &str = "\x1b[0m";
let warning = "WARN!!! device code authentication has potential risks and\n\
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we might want to improve this copy

let uc = request_user_code(&client, &auth_base_url, &opts.client_id).await?;

print_colored_warning_device_code();
println!("⏳ Generating a new 9-digit device code for authentication...\n");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this message be before the call?

.map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?;

// Try to exchange for an API key (optional)
let api_key = crate::server::obtain_api_key(&opts.issuer, &opts.client_id, &tokens.id_token)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not need to do this.

let codex_home = tempdir().unwrap();
let mock_server = MockServer::start().await;

// Mock::given(method("POST"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:rem

Copy link
Collaborator

@pakrym-oai pakrym-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@rakesh-oai
Copy link
Contributor Author

@codex review the PR

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

Comment on lines 296 to 299
if login_cli.use_device_code {
run_login_with_device_code(login_cli.config_overrides, login_cli.issuer)
.await;
} else if let Some(api_key) = login_cli.api_key {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Use existing issuer_base_url field when invoking device login

The new device-code branch calls run_login_with_device_code(login_cli.config_overrides, login_cli.issuer) even though LoginCommand only defines issuer_base_url. Because no issuer field exists on LoginCommand, this code path does not compile. Pass the issuer_base_url option (or add the field) so the CLI builds.

Useful? React with 👍 / 👎.

Comment on lines 53 to 56
) -> std::io::Result<UserCodeResp> {
let url = format!("{auth_base_url}/deviceauth/usercode");
let body = serde_json::to_string(&UserCodeReq { client_id }).map_err(std::io::Error::other)?;
let resp = client
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Serialize owned strings in device auth request payloads

Both request_user_code and poll_for_token construct UserCodeReq { client_id } / TokenPollReq { client_id, user_code } where the structs declare String fields but the variables are &str. This mismatched ownership prevents the crate from compiling. Convert the borrowed values to String (e.g. client_id.to_owned()) before serializing. The same issue appears again when building TokenPollReq a few lines below.

Useful? React with 👍 / 👎.

Comment on lines 168 to 173
let tokens = crate::server::exchange_code_for_tokens(
&opts.issuer,
&opts.client_id,
"",
&empty_pkce,
&code_resp.code,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Access nonexistent field in device code exchange

CodeSuccessResp defines authorization_code, but the login flow passes &code_resp.code to exchange_code_for_tokens. Because code is not a struct field, the file does not compile. Use the defined authorization_code (or rename the struct field to match the API payload).

Useful? React with 👍 / 👎.

Comment on lines 168 to 184
let tokens = crate::server::exchange_code_for_tokens(
&opts.issuer,
&opts.client_id,
"",
&empty_pkce,
&code_resp.code,
)
.await
.map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?;

crate::server::persist_tokens_async(
&opts.codex_home,
None,
tokens.id_token,
tokens.access_token,
tokens.refresh_token,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Device code login never exchanges tokens for an API key

After the device flow obtains tokens it calls persist_tokens_async with api_key set to None and returns. Unlike the browser-based login, obtain_api_key is never invoked, so no API key is stored and mocked integration tests expecting a second /oauth/token call will fail. Without persisting the API key the CLI cannot authenticate subsequent requests. Consider exchanging the ID token for an API key before saving credentials.

Useful? React with 👍 / 👎.

@rakesh-oai
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Sep 29, 2025
@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch 2 times, most recently from 85f6ee8 to 7dbcd97 Compare September 30, 2025 01:51
@rakesh-oai rakesh-oai enabled auto-merge (squash) September 30, 2025 01:57
@rakesh-oai rakesh-oai force-pushed the rakesh/SupportDeviceAuth branch from 7dbcd97 to bdabd77 Compare September 30, 2025 02:05
@rakesh-oai rakesh-oai merged commit 0793030 into main Sep 30, 2025
19 checks passed
@rakesh-oai rakesh-oai deleted the rakesh/SupportDeviceAuth branch September 30, 2025 02:34
@github-actions github-actions bot locked and limited conversation to collaborators Sep 30, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants