22using System . Net ;
33using System . Security . Cryptography ;
44using System . Text ;
5- using System . Web ;
65
76namespace QuotaBarWindows . Helpers ;
87
9- /// <summary>
10- /// Handles OAuth 2.0 PKCE flow: opens browser, listens for redirect, returns auth code.
11- /// </summary>
128public static class OAuthHelper
139{
1410 public static string GenerateCodeVerifier ( )
@@ -29,38 +25,50 @@ private static string Base64UrlEncode(byte[] bytes)
2925 public static string BuildAuthUrl ( string baseUrl , string clientId , string redirectUri ,
3026 string scope , string codeChallenge , string state )
3127 {
32- var query = HttpUtility . ParseQueryString ( string . Empty ) ;
33- query [ "response_type" ] = "code" ;
34- query [ "client_id" ] = clientId ;
35- query [ "redirect_uri" ] = redirectUri ;
36- query [ "scope" ] = scope ;
37- query [ "code_challenge" ] = codeChallenge ;
38- query [ "code_challenge_method" ] = "S256" ;
39- query [ "state" ] = state ;
40- return $ "{ baseUrl } ?{ query } ";
28+ static string Encode ( string s ) => Uri . EscapeDataString ( s ) ;
29+ return $ "{ baseUrl } ?response_type=code&client_id={ Encode ( clientId ) } " +
30+ $ "&redirect_uri={ Encode ( redirectUri ) } &scope={ Encode ( scope ) } " +
31+ $ "&code_challenge={ Encode ( codeChallenge ) } &code_challenge_method=S256" +
32+ $ "&state={ Encode ( state ) } ";
4133 }
4234
4335 /// <summary>
44- /// Opens the browser to the auth URL and waits for the redirect callback.
45- /// Returns the authorization code, or null if timed out / cancelled.
36+ /// Parses a query string (e.g. "?code=abc&state=xyz") and returns a key->value dictionary.
37+ /// </summary>
38+ public static Dictionary < string , string > ParseQuery ( string query )
39+ {
40+ var result = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
41+ var q = query . TrimStart ( '?' ) ;
42+ foreach ( var part in q . Split ( '&' , StringSplitOptions . RemoveEmptyEntries ) )
43+ {
44+ var idx = part . IndexOf ( '=' ) ;
45+ if ( idx < 0 ) continue ;
46+ var key = Uri . UnescapeDataString ( part [ ..idx ] ) ;
47+ var val = Uri . UnescapeDataString ( part [ ( idx + 1 ) ..] ) ;
48+ result [ key ] = val ;
49+ }
50+ return result ;
51+ }
52+
53+ /// <summary>
54+ /// Opens a local HTTP listener, waits for the OAuth redirect callback.
55+ /// Returns the authorization code or null on timeout/cancel.
4656 /// </summary>
4757 public static async Task < string ? > WaitForCallbackAsync ( string redirectUri , string expectedState ,
4858 CancellationToken ct = default )
4959 {
5060 var uri = new Uri ( redirectUri ) ;
5161 var prefix = $ "http://localhost:{ uri . Port } { uri . AbsolutePath } ";
52- if ( ! prefix . EndsWith ( '/' ) ) prefix += '/' ; // HttpListener needs trailing slash
62+ if ( ! prefix . EndsWith ( '/' ) ) prefix += '/' ;
5363
5464 using var listener = new HttpListener ( ) ;
5565 listener . Prefixes . Add ( prefix ) ;
5666
57- try
58- {
59- listener . Start ( ) ;
60- }
67+ try { listener . Start ( ) ; }
6168 catch ( Exception ex )
6269 {
63- throw new InvalidOperationException ( $ "Could not start local OAuth listener on { prefix } : { ex . Message } ") ;
70+ throw new InvalidOperationException (
71+ $ "Could not start local OAuth listener on { prefix } : { ex . Message } ") ;
6472 }
6573
6674 try
@@ -69,39 +77,32 @@ public static string BuildAuthUrl(string baseUrl, string clientId, string redire
6977 cts . CancelAfter ( TimeSpan . FromMinutes ( 5 ) ) ;
7078
7179 var contextTask = listener . GetContextAsync ( ) ;
72- var cancelTask = Task . Delay ( Timeout . Infinite , cts . Token ) ;
80+ await Task . WhenAny ( contextTask , Task . Delay ( Timeout . Infinite , cts . Token ) ) ;
81+ if ( ! contextTask . IsCompletedSuccessfully ) return null ;
7382
74- var completed = await Task . WhenAny ( contextTask , cancelTask ) ;
75- if ( completed == cancelTask ) return null ;
83+ var context = contextTask . Result ;
84+ var query = ParseQuery ( context . Request . Url ! . Query ) ;
85+ query . TryGetValue ( "code" , out var code ) ;
86+ query . TryGetValue ( "state" , out var state ) ;
7687
77- var context = await contextTask ;
78- var query = HttpUtility . ParseQueryString ( context . Request . Url ! . Query ) ;
79- var code = query [ "code" ] ;
80- var state = query [ "state" ] ;
81-
82- // Respond to the browser
8388 const string html = """
84- <html><body style="font-family:system-ui;text-align:center;padding:40px;background:#1C1C1E;color:#fff">
85- <h2>QuotaBar</h2><p>Authorization complete! You can close this tab.</p>
89+ <html><body style="font-family:system-ui;text-align:center;padding:40px;background:#F5F5F7">
90+ <h2 style="color:#1C1C1E">QuotaBar</h2>
91+ <p style="color:#8E8E93">Authorization complete! You can close this tab.</p>
8692 </body></html>
8793 """ ;
88- var responseBytes = Encoding . UTF8 . GetBytes ( html ) ;
94+ var bytes = Encoding . UTF8 . GetBytes ( html ) ;
8995 context . Response . ContentType = "text/html" ;
90- context . Response . ContentLength64 = responseBytes . Length ;
91- await context . Response . OutputStream . WriteAsync ( responseBytes , ct ) ;
96+ context . Response . ContentLength64 = bytes . Length ;
97+ await context . Response . OutputStream . WriteAsync ( bytes , ct ) ;
9298 context . Response . Close ( ) ;
9399
94100 if ( state != expectedState ) return null ;
95101 return code ;
96102 }
97- finally
98- {
99- listener . Stop ( ) ;
100- }
103+ finally { listener . Stop ( ) ; }
101104 }
102105
103106 public static void OpenBrowser ( string url )
104- {
105- Process . Start ( new ProcessStartInfo ( url ) { UseShellExecute = true } ) ;
106- }
107+ => Process . Start ( new ProcessStartInfo ( url ) { UseShellExecute = true } ) ;
107108}
0 commit comments