Skip to content

Commit 12d1ebb

Browse files
committed
user friendly msg
1 parent c378416 commit 12d1ebb

File tree

3 files changed

+80
-39
lines changed

3 files changed

+80
-39
lines changed

crates/chat-cli/src/auth/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ pub enum AuthError {
6565
Reqwest(#[from] reqwest::Error),
6666
#[error("HTTP error: {0}")]
6767
HttpStatus(reqwest::StatusCode),
68-
// Social auth specific errors
69-
#[error(
70-
"Authentication failed: The identity provider denied access. Please ensure you grant all required permissions."
71-
)]
68+
#[error("Authentication failed: {0}")]
7269
SocialAuthProviderFailure(String),
7370
}
7471

crates/chat-cli/src/auth/portal.rs

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Unified auth portal integration for streamlined authentication
22
//! Handles callbacks from https://app.kiro.dev/signin
33
4-
use std::collections::HashMap;
54
use std::time::Duration;
65

76
use bytes::Bytes;
@@ -19,6 +18,7 @@ use tokio::net::TcpListener;
1918
use tracing::{
2019
debug,
2120
info,
21+
warn,
2222
};
2323

2424
use crate::auth::AuthError;
@@ -45,17 +45,25 @@ struct AuthPortalCallback {
4545
sso_region: Option<String>,
4646
state: String,
4747
path: String,
48+
error: Option<String>,
49+
error_description: Option<String>,
4850
}
4951

5052
pub enum PortalResult {
51-
/// User authenticated with social provider (Google/GitHub)
5253
Social(SocialProvider),
53-
/// User selected BuilderID authentication
54-
BuilderId { issuer_url: String, idc_region: String },
55-
/// User selected AWS Identity Center authentication
56-
AwsIdc { issuer_url: String, idc_region: String },
57-
/// User selected internal authentication (Amazon-only)
58-
Internal { issuer_url: String, idc_region: String },
54+
BuilderId {
55+
issuer_url: String,
56+
idc_region: String,
57+
},
58+
AwsIdc {
59+
issuer_url: String,
60+
idc_region: String,
61+
},
62+
/// Internal amazon user
63+
Internal {
64+
issuer_url: String,
65+
idc_region: String,
66+
},
5967
}
6068

6169
/// Local-only: open unified portal and handle single callback
@@ -85,9 +93,51 @@ pub async fn start_unified_auth(db: &mut Database) -> Result<PortalResult, AuthE
8593

8694
let callback = wait_for_auth_callback(listener, state.clone()).await?;
8795

96+
if let Some(error) = &callback.error {
97+
let friendly_msg =
98+
format_user_friendly_error(error, callback.error_description.as_deref(), &callback.login_option);
99+
100+
warn!(
101+
"OAuth error for {}: {} - {}",
102+
callback.login_option, error, friendly_msg
103+
);
104+
105+
return Err(match callback.login_option.as_str() {
106+
"google" | "github" => AuthError::SocialAuthProviderFailure(friendly_msg),
107+
_ => AuthError::OAuthCustomError(friendly_msg),
108+
});
109+
}
110+
88111
process_portal_callback(db, callback, port, &verifier).await
89112
}
90113

114+
fn format_user_friendly_error(error_code: &str, description: Option<&str>, provider: &str) -> String {
115+
let cleaned_description = description.map(|d| {
116+
let first_part = d.split(';').next().unwrap_or(d);
117+
// Replace + with spaces (URL encoding)
118+
first_part.replace('+', " ").trim().to_string()
119+
});
120+
121+
match error_code {
122+
"access_denied" => {
123+
format!(
124+
"{} denied access to Kiro. Please ensure you grant all required permissions.",
125+
provider
126+
)
127+
},
128+
"invalid_request" => "Authentication failed due to an invalid request. Please try again.".to_string(),
129+
"unauthorized_client" => "The application is not authorized. Please contact support.".to_string(),
130+
"server_error" => {
131+
format!("{} login is temporarily unavailable. Please try again later.", provider)
132+
},
133+
"invalid_scope" => "The requested permissions are invalid. Please contact support.".to_string(),
134+
_ => {
135+
// For unknown errors, use cleaned description or a generic message
136+
cleaned_description.unwrap_or_else(|| format!("Authentication failed: {}. Please try again.", error_code))
137+
},
138+
}
139+
}
140+
91141
/// Build the authorization URL with all required parameters
92142
fn build_auth_url(redirect_base: &str, state: &str, challenge: &str) -> String {
93143
let is_internal = is_mwinit_available();
@@ -103,7 +153,6 @@ fn build_auth_url(redirect_base: &str, state: &str, challenge: &str) -> String {
103153
)
104154
}
105155

106-
/// Process the callback based on login option selected
107156
async fn process_portal_callback(
108157
db: &mut Database,
109158
callback: AuthPortalCallback,
@@ -245,7 +294,18 @@ async fn handle_valid_callback(
245294
path: &str,
246295
tx: tokio::sync::mpsc::Sender<AuthPortalCallback>,
247296
) -> Result<Response<Full<Bytes>>, AuthError> {
248-
let query_params = parse_query_params(uri);
297+
let query_params = uri
298+
.query()
299+
.map(|query| {
300+
query
301+
.split('&')
302+
.filter_map(|kv| {
303+
kv.split_once('=')
304+
.map(|(k, v)| (k.to_string(), urlencoding::decode(v).unwrap_or_default().to_string()))
305+
})
306+
.collect::<std::collections::HashMap<String, String>>() //
307+
})
308+
.ok_or(AuthError::OAuthCustomError("query parameters are missing".into()))?;
249309

250310
let callback = AuthPortalCallback {
251311
login_option: query_params.get("login_option").cloned().unwrap_or_default(),
@@ -254,22 +314,20 @@ async fn handle_valid_callback(
254314
sso_region: query_params.get("idc_region").cloned(),
255315
state: query_params.get("state").cloned().unwrap_or_default(),
256316
path: path.to_string(),
317+
error: query_params.get("error").cloned(),
318+
error_description: query_params.get("error_description").cloned(),
257319
};
258320

259-
debug!(
260-
login_option=%callback.login_option,
261-
code_present=%callback.code.is_some(),
262-
issuer_url=?callback.issuer_url,
263-
state=%callback.state,
264-
"Parsed portal callback query"
265-
);
266-
267-
let _ = tx.send(callback).await;
321+
let _ = tx.send(callback.clone()).await;
268322

269-
build_redirect_response("success", None)
323+
if let Some(error) = &callback.error {
324+
let error_msg = callback.error_description.as_deref().unwrap_or(error.as_str());
325+
build_redirect_response("error", Some(error_msg))
326+
} else {
327+
build_redirect_response("success", None)
328+
}
270329
}
271330

272-
/// Handle invalid callback paths
273331
async fn handle_invalid_callback(path: &str) -> Result<Response<Full<Bytes>>, AuthError> {
274332
info!(%path, "Invalid callback path, redirecting to portal");
275333
build_redirect_response("error", Some("Invalid callback path"))
@@ -291,18 +349,6 @@ fn build_redirect_response(status: &str, error_message: Option<&str>) -> Result<
291349
.expect("valid response"))
292350
}
293351

294-
/// Parse query parameters from URI
295-
fn parse_query_params(uri: &hyper::Uri) -> HashMap<String, String> {
296-
uri.query()
297-
.map(|q| {
298-
q.split('&')
299-
.filter_map(|kv| kv.split_once('='))
300-
.map(|(k, v)| (k.to_string(), urlencoding::decode(v).unwrap_or_default().to_string()))
301-
.collect()
302-
})
303-
.unwrap_or_default()
304-
}
305-
306352
async fn bind_allowed_port(ports: &[u16]) -> Result<TcpListener, AuthError> {
307353
for port in ports {
308354
match TcpListener::bind(("127.0.0.1", *port)).await {

crates/chat-cli/src/auth/social.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ impl SocialToken {
200200
"redirect_uri": redirect_uri,
201201
});
202202

203-
// Send request
204203
let response = client
205204
.post(format!("{}/oauth/token", SOCIAL_AUTH_SERVICE_ENDPOINT))
206205
.header("Content-Type", "application/json")
@@ -209,7 +208,6 @@ impl SocialToken {
209208
.send()
210209
.await?;
211210

212-
// Handle response
213211
if !response.status().is_success() {
214212
let status = response.status();
215213
let body = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());

0 commit comments

Comments
 (0)