133 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			133 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System.IdentityModel.Tokens.Jwt;
 | 
						|
using DysonNetwork.Shared.Cache;
 | 
						|
using Microsoft.IdentityModel.Tokens;
 | 
						|
 | 
						|
namespace DysonNetwork.Pass.Auth.OpenId;
 | 
						|
 | 
						|
public class GoogleOidcService(
 | 
						|
    IConfiguration configuration,
 | 
						|
    IHttpClientFactory httpClientFactory,
 | 
						|
    AppDatabase db,
 | 
						|
    AuthService auth,
 | 
						|
    ICacheService cache
 | 
						|
)
 | 
						|
    : OidcService(configuration, httpClientFactory, db, auth, cache)
 | 
						|
{
 | 
						|
    private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
 | 
						|
 | 
						|
    public override string ProviderName => "google";
 | 
						|
    protected override string DiscoveryEndpoint => "https://accounts.google.com/.well-known/openid-configuration";
 | 
						|
    protected override string ConfigSectionName => "Google";
 | 
						|
 | 
						|
    public override string GetAuthorizationUrl(string state, string nonce)
 | 
						|
    {
 | 
						|
        var config = GetProviderConfig();
 | 
						|
        var discoveryDocument = GetDiscoveryDocumentAsync().GetAwaiter().GetResult();
 | 
						|
 | 
						|
        if (discoveryDocument?.AuthorizationEndpoint == null)
 | 
						|
        {
 | 
						|
            throw new InvalidOperationException("Authorization endpoint not found in discovery document");
 | 
						|
        }
 | 
						|
 | 
						|
        var queryParams = new Dictionary<string, string>
 | 
						|
        {
 | 
						|
            { "client_id", config.ClientId },
 | 
						|
            { "redirect_uri", config.RedirectUri },
 | 
						|
            { "response_type", "code" },
 | 
						|
            { "scope", "openid email profile" },
 | 
						|
            { "state", state }, // No '|codeVerifier' appended anymore
 | 
						|
            { "nonce", nonce }
 | 
						|
        };
 | 
						|
 | 
						|
        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);
 | 
						|
        if (tokenResponse?.IdToken == null)
 | 
						|
        {
 | 
						|
            throw new InvalidOperationException("Failed to obtain ID token from Google");
 | 
						|
        }
 | 
						|
 | 
						|
        // 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)
 | 
						|
    {
 | 
						|
        var discoveryDocument = await GetDiscoveryDocumentAsync();
 | 
						|
        if (discoveryDocument?.JwksUri == null)
 | 
						|
        {
 | 
						|
            throw new InvalidOperationException("JWKS URI not found in discovery document");
 | 
						|
        }
 | 
						|
 | 
						|
        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);
 | 
						|
    }
 | 
						|
} |