11//! Unified auth portal integration for streamlined authentication
22//! Handles callbacks from https://app.kiro.dev/signin
33
4- use std:: collections:: HashMap ;
54use std:: time:: Duration ;
65
76use bytes:: Bytes ;
@@ -19,6 +18,7 @@ use tokio::net::TcpListener;
1918use tracing:: {
2019 debug,
2120 info,
21+ warn,
2222} ;
2323
2424use 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
5052pub 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
92142fn 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
107156async 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
273331async 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-
306352async fn bind_allowed_port ( ports : & [ u16 ] ) -> Result < TcpListener , AuthError > {
307353 for port in ports {
308354 match TcpListener :: bind ( ( "127.0.0.1" , * port) ) . await {
0 commit comments