♻️ Refactor OpenID: Phase 2: Security Hardening - PKCE Implementation
- Added GenerateCodeVerifier() and GenerateCodeChallenge() methods to base OidcService - Implemented PKCE (Proof Key for Code Exchange) for Google OAuth flow: * Generate cryptographically secure code verifier (256-bit random) * Create SHA-256 code challenge for authorization request * Cache code verifier with 15-minute expiration for token exchange * Validate and remove code verifier during callback to prevent replay attacks - Enhances security by protecting against authorization code interception attacks - Uses S256 (SHA-256) code challenge method as per RFC 7636
This commit is contained in:
		@@ -29,6 +29,10 @@ public class GoogleOidcService(
 | 
				
			|||||||
            throw new InvalidOperationException("Authorization endpoint not found in discovery document");
 | 
					            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(
 | 
					        var queryParams = BuildAuthorizationParameters(
 | 
				
			||||||
            config.ClientId,
 | 
					            config.ClientId,
 | 
				
			||||||
            config.RedirectUri,
 | 
					            config.RedirectUri,
 | 
				
			||||||
@@ -38,19 +42,36 @@ public class GoogleOidcService(
 | 
				
			|||||||
            nonce
 | 
					            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)}"));
 | 
					        var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
 | 
				
			||||||
        return $"{discoveryDocument.AuthorizationEndpoint}?{queryString}";
 | 
					        return $"{discoveryDocument.AuthorizationEndpoint}?{queryString}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
 | 
					    public override async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        // No need to split or parse code verifier from state
 | 
					 | 
				
			||||||
        var state = callbackData.State ?? "";
 | 
					        var state = callbackData.State ?? "";
 | 
				
			||||||
        callbackData.State = state; // Keep the original state if needed
 | 
					        callbackData.State = state; // Keep the original state if needed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Exchange the code for tokens
 | 
					        // Retrieve PKCE code verifier from cache
 | 
				
			||||||
        // Pass null or omit the parameter for codeVerifier as PKCE is removed
 | 
					        var codeVerifierKey = $"pkce:{state}";
 | 
				
			||||||
        var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code, null);
 | 
					        var (found, codeVerifier) = await cache.GetAsyncWithStatus<string>(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)
 | 
					        if (tokenResponse?.IdToken == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            throw new InvalidOperationException("Failed to obtain ID token from Google");
 | 
					            throw new InvalidOperationException("Failed to obtain ID token from Google");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
 | 
					using System;
 | 
				
			||||||
using System.IdentityModel.Tokens.Jwt;
 | 
					using System.IdentityModel.Tokens.Jwt;
 | 
				
			||||||
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
using System.Text.Json.Serialization;
 | 
					using System.Text.Json.Serialization;
 | 
				
			||||||
using DysonNetwork.Shared.Cache;
 | 
					using DysonNetwork.Shared.Cache;
 | 
				
			||||||
using DysonNetwork.Shared.Models;
 | 
					using DysonNetwork.Shared.Models;
 | 
				
			||||||
@@ -84,6 +87,38 @@ public abstract class OidcService(
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Generates a cryptographically secure random code verifier for PKCE
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    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('=');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// <summary>
 | 
				
			||||||
 | 
					    /// Generates the code challenge from a code verifier using S256 method
 | 
				
			||||||
 | 
					    /// </summary>
 | 
				
			||||||
 | 
					    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('=');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Retrieves the OpenID Connect discovery document
 | 
					    /// Retrieves the OpenID Connect discovery document
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user