.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

