♻️ 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