class Program { static async Task Main(string[] args) { var _usePKCE = false; // ====================================== var clientId = "clientId"; var clientSecret = "clientSecret"; //_usePKCE = true; // for SPA/Mobile (without backend) // ====================================== var authorizationEndpoint = "http://localhost:8090/authorize"; var tokenEndpoint = "http://localhost:8090/token"; var redirectUri = "http://localhost:8080/"; var scope = "openid profile email"; // Adjust the scope based on your needs var state = Guid.NewGuid().ToString(); var authorizeUrl = $"{authorizationEndpoint}?client_id={clientId}&redirect_uri={HttpUtility.UrlEncode(redirectUri)}&response_type=code&scope={System.Web.HttpUtility.UrlEncode(scope)}&state={state}"; var code_verifier = ""; var code_challenge = ""; if (_usePKCE) { code_verifier = getCodeVerifier(); code_challenge = getCodeChallenge(code_verifier); authorizeUrl += $"&code_challenge={code_challenge}&code_challenge_method=S256 "; } Clipboard.SetText(authorizeUrl); Console.WriteLine("Please open the following URL in your browser to authenticate: [ already in your clipboard ;) ] \n\n"); Console.WriteLine(authorizeUrl + "\n\n"); using (var listener = new HttpListener()) { listener.Prefixes.Add(redirectUri); listener.Start(); Console.WriteLine("Waiting for callback..."); while (true) { var context = await listener.GetContextAsync(); var request = context.Request; if (request.HttpMethod == "GET") { var queryParameters = HttpUtility.ParseQueryString(request.Url.Query); var authorizationCode = queryParameters["code"]; var receivedState = queryParameters["state"]; var tokenContent = ""; if (receivedState == state) { // Exchange authorization code for access token Console.WriteLine($"Received authorization code: {authorizationCode}\n\n"); var tokenClient = new System.Net.Http.HttpClient(); var tokenRequest = new Dictionary { { "grant_type", "authorization_code" }, { "code", authorizationCode }, { "redirect_uri", redirectUri }, { "client_id", clientId } }; if (_usePKCE) { tokenRequest.Add("code_verifier", code_verifier); } else { tokenRequest.Add("client_secret", clientSecret); } var tokenResponse = await tokenClient.PostAsync(tokenEndpoint, new FormUrlEncodedContent(tokenRequest)); tokenContent = await tokenResponse.Content.ReadAsStringAsync(); Console.WriteLine($"Token Response: {tokenContent}"); } else { Console.WriteLine("Received state does not match."); } string responseString = "Authorization successful.\nJWT can be decoded at: https://jwt.io/ \nResponse:\n\n" + tokenContent; byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString); context.Response.AddHeader("Content-Type", "application/json; charset=utf-8"); context.Response.ContentLength64 = buffer.Length; context.Response.OutputStream.Write(buffer, 0, buffer.Length); context.Response.Close(); break; } } listener.Stop(); } } static string getCodeVerifier() { var rng = RandomNumberGenerator.Create(); var bytes = new byte[32]; rng.GetBytes(bytes); // It is recommended to use a URL-safe string as code_verifier. // See section 4 of RFC 7636 for more details. return Convert.ToBase64String(bytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } static string getCodeChallenge(string val) { using (var sha256 = SHA256.Create()) { var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(val)); return Convert.ToBase64String(challengeBytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } } }