♻️ 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:
2025-11-02 14:55:15 +08:00
parent 4bd59f107b
commit 74a9ca98ad
2 changed files with 60 additions and 4 deletions

View File

@@ -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<OidcUserInfo> 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<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)
{
throw new InvalidOperationException("Failed to obtain ID token from Google");

View File

@@ -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(
};
}
/// <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>
/// Retrieves the OpenID Connect discovery document
/// </summary>