Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ pub struct AppConfig {
/// Telegram bot username (without @) for link instructions.
pub telegram_bot_username: Option<String>,

/// OpenAI plugin-portal domain verification token served verbatim at
/// `/.well-known/openai-apps-challenge`. Unset = route returns 404.
pub openai_apps_challenge_token: Option<String>,

/// Interval in seconds between approval expiry sweeps (default: 5).
pub approval_expiry_interval_secs: u64,

Expand Down Expand Up @@ -717,6 +721,10 @@ impl std::fmt::Debug for AppConfig {
.field("telegram_webhook_secret", &"[REDACTED]")
.field("telegram_webhook_url", &self.telegram_webhook_url)
.field("telegram_bot_username", &self.telegram_bot_username)
.field(
"openai_apps_challenge_token",
&self.openai_apps_challenge_token,
)
.field(
"approval_expiry_interval_secs",
&self.approval_expiry_interval_secs,
Expand Down Expand Up @@ -1200,6 +1208,10 @@ impl AppConfig {
telegram_bot_username: env::var("TELEGRAM_BOT_USERNAME")
.ok()
.filter(|s| !s.is_empty()),
openai_apps_challenge_token: env::var("OPENAI_APPS_CHALLENGE_TOKEN")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),

approval_expiry_interval_secs: env::var("APPROVAL_EXPIRY_INTERVAL_SECS")
.ok()
Expand Down Expand Up @@ -1946,6 +1958,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/crypto/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/crypto/apple_client_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ ci0O2dgc19c2/sLtanU7P2KAzhEo8O0tIc0Dwe/nMqKfue82eGVL3DqM\n\
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/crypto/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/crypto/local_key_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/handlers/channel_webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
53 changes: 51 additions & 2 deletions backend/src/handlers/oidc_discovery.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use axum::{Json, extract::State};
use axum::{
Json,
extract::State,
http::{StatusCode, header},
response::{IntoResponse, Response},
};

use crate::AppState;
use crate::services::oauth_broker_service;
Expand Down Expand Up @@ -136,11 +141,27 @@ pub async fn jwks(State(state): State<AppState>) -> Json<serde_json::Value> {
}))
}

/// GET /.well-known/openai-apps-challenge
///
/// OpenAI plugin-portal domain verification for the MCP host. Returns the
/// configured token as bare plain text, or 404 when none is configured.
pub async fn openai_apps_challenge(State(state): State<AppState>) -> Response {
match &state.config.openai_apps_challenge_token {
Some(token) => (
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
token.clone(),
)
.into_response(),
None => StatusCode::NOT_FOUND.into_response(),
}
}

#[cfg(test)]
mod tests {
use super::{
OAUTH_AUTHORIZATION_SERVER_SCOPES_SUPPORTED, OPENID_CONFIGURATION_SCOPES_SUPPORTED,
oauth_authorization_server_metadata, oauth_protected_resource, openid_configuration,
oauth_authorization_server_metadata, oauth_protected_resource, openai_apps_challenge,
openid_configuration,
};
use crate::services::oauth_broker_service::{BROKER_BINDING_SCOPE, BROKER_SUBJECT_TOKEN_TYPE};
use axum::extract::State;
Expand All @@ -151,6 +172,34 @@ mod tests {
assert!(!OAUTH_AUTHORIZATION_SERVER_SCOPES_SUPPORTED.contains(&BROKER_BINDING_SCOPE));
}

#[tokio::test]
async fn openai_apps_challenge_serves_configured_token_as_plain_text() {
let mut state = crate::test_utils::test_app_state_no_db().await;
state.config.openai_apps_challenge_token = Some("challenge-token-123".to_string());

let response = openai_apps_challenge(State(state)).await;

assert_eq!(response.status(), axum::http::StatusCode::OK);
assert_eq!(
response.headers()[axum::http::header::CONTENT_TYPE],
"text/plain; charset=utf-8"
);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
assert_eq!(&body[..], b"challenge-token-123");
}

#[tokio::test]
async fn openai_apps_challenge_is_not_found_when_unconfigured() {
let mut state = crate::test_utils::test_app_state_no_db().await;
state.config.openai_apps_challenge_token = None;

let response = openai_apps_challenge(State(state)).await;

assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn protected_resource_metadata_advertises_mcp_default_scopes() {
// NyxID#1226: clients pick their requested scopes from this list, so
Expand Down
4 changes: 4 additions & 0 deletions backend/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,10 @@ fn build_router_internal(router_state: Option<AppState>) -> (Router<AppState>, R
.route(
"/oauth-protected-resource",
get(handlers::oidc_discovery::oauth_protected_resource),
)
.route(
"/openai-apps-challenge",
get(handlers::oidc_discovery::openai_apps_challenge),
);

let public_oauth = Router::new()
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/channel_relay_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/social_auth_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ mod tests {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions backend/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,7 @@ pub(crate) fn test_app_config() -> AppConfig {
telegram_webhook_secret: None,
telegram_webhook_url: None,
telegram_bot_username: None,
openai_apps_challenge_token: None,
approval_expiry_interval_secs: 5,
connect_link_expiry_sweep_interval_secs: 60,
agent_key_login_sweep_interval_secs: 60,
Expand Down
1 change: 1 addition & 0 deletions docs/ENV.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ Requires all four values. Create a Services ID and key at the [Apple Developer p
| `TELEGRAM_WEBHOOK_SECRET` | | Secret for verifying Telegram webhook callbacks |
| `TELEGRAM_WEBHOOK_URL` | | Public URL for Telegram webhooks (e.g. `https://auth.nyxid.dev/api/v1/webhooks/telegram`). Omit to use long polling mode. |
| `TELEGRAM_BOT_USERNAME` | | Bot username without @ (for link instructions) |
| `OPENAI_APPS_CHALLENGE_TOKEN` | | OpenAI plugin-portal domain verification token, served as plain text at `/.well-known/openai-apps-challenge` on the MCP host. Unset = 404 |
| `APPROVAL_EXPIRY_INTERVAL_SECS` | `5` | Interval between approval expiry sweeps (seconds) |

The approval system works without Telegram -- users can always approve/reject via the web UI. Telegram delivery requires `TELEGRAM_BOT_TOKEN`.
Expand Down
Loading