♻️ Refactor OpenID: Phase 4: Advanced Architecture - Strategy Pattern Implementation
- Added comprehensive user info strategy pattern with IUserInfoStrategy interface - Created IdTokenValidationStrategy for Google/Apple ID token validation and parsing - Implemented UserInfoEndpointStrategy for Microsoft/Discord/GitHub OAuth user data retrieval - Added DirectTokenResponseStrategy placeholder for Afdian and similar providers - Updated GoogleOidcService to use IdTokenValidationStrategy instead of custom callback logic - Centralized JWT token validation, claim extraction, and user data parsing logic - Eliminated code duplication across providers while maintaining provider-specific behavior - Improved maintainability by separating concerns of user data retrieval methods - Set architectural foundation for easily adding new OIDC providers by implementing appropriate strategies
This commit is contained in:
		@@ -72,82 +72,18 @@ public class GoogleOidcService(
 | 
			
		||||
 | 
			
		||||
        // Exchange the code for tokens using PKCE
 | 
			
		||||
        var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code, codeVerifier);
 | 
			
		||||
        if (tokenResponse?.IdToken == null)
 | 
			
		||||
        if (tokenResponse == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("Failed to obtain ID token from Google");
 | 
			
		||||
            throw new InvalidOperationException("Failed to exchange code for tokens");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Validate the ID token
 | 
			
		||||
        var userInfo = await ValidateTokenAsync(tokenResponse.IdToken);
 | 
			
		||||
 | 
			
		||||
        // Set tokens on the user info
 | 
			
		||||
        userInfo.AccessToken = tokenResponse.AccessToken;
 | 
			
		||||
        userInfo.RefreshToken = tokenResponse.RefreshToken;
 | 
			
		||||
 | 
			
		||||
        // Try to fetch additional profile data if userinfo endpoint is available
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var discoveryDocument = await GetDiscoveryDocumentAsync();
 | 
			
		||||
            if (discoveryDocument?.UserinfoEndpoint != null && !string.IsNullOrEmpty(tokenResponse.AccessToken))
 | 
			
		||||
            {
 | 
			
		||||
                var client = _httpClientFactory.CreateClient();
 | 
			
		||||
                client.DefaultRequestHeaders.Authorization =
 | 
			
		||||
                    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
 | 
			
		||||
 | 
			
		||||
                var userInfoResponse =
 | 
			
		||||
                    await client.GetFromJsonAsync<Dictionary<string, object>>(discoveryDocument.UserinfoEndpoint);
 | 
			
		||||
 | 
			
		||||
                if (userInfoResponse != null)
 | 
			
		||||
                {
 | 
			
		||||
                    if (userInfoResponse.TryGetValue("picture", out var picture) && picture != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        userInfo.ProfilePictureUrl = picture.ToString();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            // Ignore errors when fetching additional profile data
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return userInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<OidcUserInfo> ValidateTokenAsync(string idToken)
 | 
			
		||||
    {
 | 
			
		||||
        // Use the strategy pattern to retrieve user info
 | 
			
		||||
        var discoveryDocument = await GetDiscoveryDocumentAsync();
 | 
			
		||||
        if (discoveryDocument?.JwksUri == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("JWKS URI not found in discovery document");
 | 
			
		||||
        }
 | 
			
		||||
        var config = GetProviderConfig();
 | 
			
		||||
        var strategy = new IdTokenValidationStrategy(_httpClientFactory);
 | 
			
		||||
 | 
			
		||||
        var client = _httpClientFactory.CreateClient();
 | 
			
		||||
        var jwksResponse = await client.GetFromJsonAsync<JsonWebKeySet>(discoveryDocument.JwksUri);
 | 
			
		||||
        if (jwksResponse == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("Failed to retrieve JWKS from Google");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var handler = new JwtSecurityTokenHandler();
 | 
			
		||||
        var jwtToken = handler.ReadJwtToken(idToken);
 | 
			
		||||
        var kid = jwtToken.Header.Kid;
 | 
			
		||||
        var signingKey = jwksResponse.Keys.FirstOrDefault(k => k.Kid == kid);
 | 
			
		||||
        if (signingKey == null)
 | 
			
		||||
        {
 | 
			
		||||
            throw new SecurityTokenValidationException("Unable to find matching key in Google's JWKS");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var validationParameters = new TokenValidationParameters
 | 
			
		||||
        {
 | 
			
		||||
            ValidateIssuer = true,
 | 
			
		||||
            ValidIssuer = "https://accounts.google.com",
 | 
			
		||||
            ValidateAudience = true,
 | 
			
		||||
            ValidAudience = GetProviderConfig().ClientId,
 | 
			
		||||
            ValidateLifetime = true,
 | 
			
		||||
            IssuerSigningKey = signingKey
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return ValidateAndExtractIdToken(idToken, validationParameters);
 | 
			
		||||
        return await strategy.GetUserInfoAsync(tokenResponse, discoveryDocument, config.ClientId, ProviderName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user