You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI调用Google Play Developer API时OAuth客户端类型错误排查

问题描述

正在开发.NET MAUI移动应用,尝试生成OAuth 2.0认证令牌以调用Google Play Developer API,已完成以下配置:

  • 在Google Cloud Console创建项目;
  • 启用Google Play Developer API;
  • 配置OAuth同意屏幕为外部应用类型,并添加https://www.googleapis.com/auth/androidpublisher作为权限范围;
  • 创建OAuth 2.0客户端ID,应用类型设为Android,并添加包名与SHA-1指纹。

项目已安装的NuGet包:

  • Microsoft.Identity.Client
  • System.Net.Http.Json

实现设备码认证流程时遇到错误:

Error requesting device code:
{
"error": "invalid_client",
"error_description": "Invalid client type."
}
Failed to acquire access token.

设备码认证实现代码:

public class GoogleAuthService
{
    private static readonly HttpClient _httpClient = new HttpClient();

    // Set your Client ID
    private const string ClientId = "Your_Google_Client_ID";

    // Set Google OAuth endpoints
    private const string DeviceCodeEndpoint = "https://oauth2.googleapis.com/device/code";
    private const string TokenEndpoint = "https://oauth2.googleapis.com/token";

    // Scopes needed for the Google Play Developer API
    private const string Scope = "https://www.googleapis.com/auth/androidpublisher";

    public async Task<string> GetAccessTokenAsync()
    {
        // Step 1: Request the device code
        var deviceCodeResponse = await RequestDeviceCodeAsync();

        if (deviceCodeResponse != null)
        {
            var deviceCode = deviceCodeResponse["device_code"].ToString();
            var userCode = deviceCodeResponse["user_code"].ToString();
            var verificationUrl = deviceCodeResponse["verification_url"].ToString();
            var expiresIn = deviceCodeResponse["expires_in"].ToString();
            var interval = int.Parse(deviceCodeResponse["interval"].ToString());

            // Instruct the user to visit the URL and input the user code
            Console.WriteLine($"Please visit {verificationUrl} and enter the code: {userCode}");

            // Step 2: Poll for the token using the device code
            return await PollForAccessTokenAsync(deviceCode, interval);
        }

        return null;
    }

    private async Task<JObject> RequestDeviceCodeAsync()
    {
        var parameters = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("client_id", ClientId),
            new KeyValuePair<string, string>("scope", Scope)
        });

        var response = await _httpClient.PostAsync(DeviceCodeEndpoint, parameters);
        var responseBody = await response.Content.ReadAsStringAsync();

        if (response.IsSuccessStatusCode)
        {
            return JObject.Parse(responseBody);  // Return the device code response
        }

        Console.WriteLine($"Error requesting device code: {responseBody}");
        return null;
    }

    private async Task<string> PollForAccessTokenAsync(string deviceCode, int interval)
    {
        while (true)
        {
            await Task.Delay(interval * 1000);  // Wait for the polling interval

            var parameters = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("client_id", ClientId),
                new KeyValuePair<string, string>("device_code", deviceCode),
                new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
            });

            var response = await _httpClient.PostAsync(TokenEndpoint, parameters);
            var responseBody = await response.Content.ReadAsStringAsync();

            if (response.IsSuccessStatusCode)
            {
                var tokenResponse = JObject.Parse(responseBody);
                var accessToken = tokenResponse["access_token"].ToString();
                Console.WriteLine($"Access Token: {accessToken}");
                return accessToken;  // Return the access token
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
            {
                var error = JObject.Parse(responseBody)["error"].ToString();
                if (error == "authorization_pending")
                {
                    Console.WriteLine("Authorization pending, waiting...");
                    continue;  // Keep polling
                }
                else
                {
                    Console.WriteLine($"Error: {error}");
                    return null;
                }
            }
        }
    }
}

调用代码:

public async void LoginButtonClicked(Object sender, EventArgs e)
{
    string accessToken = await _googleAuthService.GetAccessTokenAsync();

    if (!string.IsNullOrEmpty(accessToken))
    {
        // Use the access token to make API calls to Google Play Developer API
        Console.WriteLine($"Access Token: {accessToken}");
    }
    else
    {
        // Handle failure
        Console.WriteLine("Failed to acquire access token.");
    }
}

AndroidManifest.xml配置:

<activity android:name="MainActivity"
  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="com.googleusercontent.apps.OAuth 2.0 client ID" />
        <data android:host="oauth2redirect" />
    </intent-filter>
</activity>

请问是什么原因导致这个"invalid_client"错误?在为.NET MAUI移动应用配置OAuth 2.0认证令牌以调用Google Play Developer API时,是否遗漏了什么?


问题分析与解决方案

核心原因

你遇到的invalid_client错误,本质是Android类型的OAuth客户端ID不支持设备码授权流程。Google的设备码授权(Device Authorization Grant)仅允许桌面应用、CLI工具这类没有浏览器或无法直接跳转授权页面的应用使用,Android原生应用客户端ID不在支持范围内。

修复步骤

1. 创建正确的OAuth客户端ID

在Google Cloud Console中,新增一个桌面应用类型的OAuth客户端ID:

  • 进入项目的「API和服务」→「凭据」页面;
  • 点击「创建凭据」→「OAuth客户端ID」;
  • 应用类型选择「桌面应用」,填写名称后保存;
  • 用这个新的客户端ID替换代码中ClientId常量的值。

2. 用MSAL简化认证流程(推荐)

既然已经安装了Microsoft.Identity.Client(MSAL),完全可以用它来简化设备码认证流程,不用自己手动实现HTTP请求:

public class GoogleAuthService
{
    private readonly IPublicClientApplication _msalClient;
    private const string Scope = "https://www.googleapis.com/auth/androidpublisher";

    public GoogleAuthService()
    {
        _msalClient = PublicClientApplicationBuilder
            .Create("你的桌面应用客户端ID")
            .WithAuthority(AzureCloudInstance.AzurePublic, "google.com")
            .Build();
    }

    public async Task<string> GetAccessTokenAsync()
    {
        var accounts = await _msalClient.GetAccountsAsync();
        AuthenticationResult result;

        try
        {
            // 尝试静默获取令牌
            result = await _msalClient.AcquireTokenSilent(new[] { Scope }, accounts.FirstOrDefault())
                .ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // 触发设备码流程
            result = await _msalClient.AcquireTokenWithDeviceCode(new[] { Scope }, deviceCodeResult =>
            {
                Console.WriteLine(deviceCodeResult.Message);
                return Task.CompletedTask;
            }).ExecuteAsync();
        }

        return result.AccessToken;
    }
}

3. 清理无用配置

设备码流程不需要跳转回调,所以之前在AndroidManifest.xml中配置的<intent-filter>可以删除,避免冗余。

额外注意事项

  • 确保OAuth同意屏幕已发布:外部类型的应用需要提交审核并发布后,才能让非测试用户使用;测试阶段可在同意屏幕页面添加测试用户邮箱;
  • 验证权限范围:确认https://www.googleapis.com/auth/androidpublisher已正确添加到OAuth同意屏幕的权限列表中;
  • 避免使用async void:LoginButtonClicked方法用async void会导致异常无法被捕获,建议改成async Task:
public async Task LoginButtonClicked(Object sender, EventArgs e)
{
    // 原逻辑不变
}

内容的提问来源于stack exchange,提问作者Sakshi Poojary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:54:54