diff --git a/DysonNetwork.Pass/Auth/OpenId/GoogleOidcService.cs b/DysonNetwork.Pass/Auth/OpenId/GoogleOidcService.cs index 83a7d58..86bc5ad 100644 --- a/DysonNetwork.Pass/Auth/OpenId/GoogleOidcService.cs +++ b/DysonNetwork.Pass/Auth/OpenId/GoogleOidcService.cs @@ -29,6 +29,10 @@ public class GoogleOidcService( throw new InvalidOperationException("Authorization endpoint not found in discovery document"); } + // Generate PKCE code verifier and challenge for enhanced security + var codeVerifier = GenerateCodeVerifier(); + var codeChallenge = GenerateCodeChallenge(codeVerifier); + var queryParams = BuildAuthorizationParameters( config.ClientId, config.RedirectUri, @@ -38,19 +42,36 @@ public class GoogleOidcService( nonce ); + // Add PKCE parameters + queryParams["code_challenge"] = codeChallenge; + queryParams["code_challenge_method"] = "S256"; + + // Store code verifier in cache for later token exchange + var codeVerifierKey = $"pkce:{state}"; + cache.SetAsync(codeVerifierKey, codeVerifier, TimeSpan.FromMinutes(15)).GetAwaiter().GetResult(); + var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); return $"{discoveryDocument.AuthorizationEndpoint}?{queryString}"; } public override async Task ProcessCallbackAsync(OidcCallbackData callbackData) { - // No need to split or parse code verifier from state var state = callbackData.State ?? ""; callbackData.State = state; // Keep the original state if needed - // Exchange the code for tokens - // Pass null or omit the parameter for codeVerifier as PKCE is removed - var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code, null); + // Retrieve PKCE code verifier from cache + var codeVerifierKey = $"pkce:{state}"; + var (found, codeVerifier) = await cache.GetAsyncWithStatus(codeVerifierKey); + if (!found || string.IsNullOrEmpty(codeVerifier)) + { + throw new InvalidOperationException("PKCE code verifier not found or expired"); + } + + // Remove the code verifier from cache to prevent replay attacks + await cache.RemoveAsync(codeVerifierKey); + + // Exchange the code for tokens using PKCE + var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code, codeVerifier); if (tokenResponse?.IdToken == null) { throw new InvalidOperationException("Failed to obtain ID token from Google"); diff --git a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs index 2824551..24ee21c 100644 --- a/DysonNetwork.Pass/Auth/OpenId/OidcService.cs +++ b/DysonNetwork.Pass/Auth/OpenId/OidcService.cs @@ -1,4 +1,7 @@ +using System; using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using System.Text; using System.Text.Json.Serialization; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; @@ -84,6 +87,38 @@ public abstract class OidcService( }; } + /// + /// Generates a cryptographically secure random code verifier for PKCE + /// + protected static string GenerateCodeVerifier() + { + // Generate a 32-byte (256-bit) random byte array + var randomBytes = new byte[32]; + RandomNumberGenerator.Fill(randomBytes); + + // Convert to URL-safe base64 (no padding) + return Convert.ToBase64String(randomBytes) + .Replace("+", "-") + .Replace("/", "_") + .TrimEnd('='); + } + + /// + /// Generates the code challenge from a code verifier using S256 method + /// + protected static string GenerateCodeChallenge(string codeVerifier) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(codeVerifier); + var hash = sha256.ComputeHash(bytes); + + // Convert to URL-safe base64 (no padding) + return Convert.ToBase64String(hash) + .Replace("+", "-") + .Replace("/", "_") + .TrimEnd('='); + } + /// /// Retrieves the OpenID Connect discovery document ///