ASP.NET Core 3.1集成LinkedIn外部登录时GetExternalLoginInfoAsync返回null
GetExternalLoginInfoAsync()返回null的问题 这个问题我之前帮不少开发者排查过,GetExternalLoginInfoAsync()返回null大多是因为OAuth流程里的用户信息映射没适配LinkedIn的API规则,或者权限、回调配置有疏漏。咱们一步步来修复:
1. 升级到LinkedIn v2 API并配置字段映射
你当前用的是v1版本的用户信息端点,LinkedIn已经逐步废弃v1接口,而且v2的响应格式和v1完全不同,默认的OAuth处理逻辑没法自动解析。
首先把用户信息端点换成v2的:
options.UserInformationEndpoint = "https://api.linkedin.com/v2/userinfo";
然后添加Claim映射规则,把LinkedIn返回的字段对应到ASP.NET的Claim类型:
// 映射LinkedIn返回的核心用户字段到Claims options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); options.ClaimActions.MapJsonKey("urn:linkedin:picture", "picture");
最后要自定义OnCreatingTicket事件,因为LinkedIn v2 API要求请求头必须指定application/json格式:
options.Events = new OAuthEvents { OnCreatingTicket = async context => { var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted); response.EnsureSuccessStatusCode(); var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; context.RunClaimActions(user); } };
2. 精简不必要的Scope权限
你配置了w_member_social权限,但这个是用来发布内容到LinkedIn的,如果你只是做登录验证,完全不需要这个权限。多余的权限不仅会增加授权弹窗的复杂度,还可能导致权限验证失败,建议只保留必要的:
options.Scope.Add("r_liteprofile"); options.Scope.Add("r_emailaddress");
3. 确认LinkedIn应用的回调URL完全匹配
在LinkedIn开发者后台的应用设置里,授权回调URL必须和你代码里的CallbackPath完全一致,包括协议(http/https)、端口、路径。比如你的站点是https://localhost:5001,回调URL就必须是https://localhost:5001/signin-linkedin,差一个字符都不行。
4. 检查Cookie和中间件配置
ASP.NET Core的外部登录依赖Cookie保存中间状态,确保你的Cookie配置正确,并且中间件顺序没错:
- 在
ConfigureServices里确认Cookie配置:services.ConfigureApplicationCookie(options => { options.LoginPath = "/Account/Login"; options.LogoutPath = "/Account/Logout"; options.AccessDeniedPath = "/Account/AccessDenied"; }); - 在
Configure方法里确保中间件顺序正确:app.UseAuthentication(); // 必须在UseAuthorization之前 app.UseAuthorization();
完整的配置示例
把上面的修改整合后,完整的LinkedIn OAuth配置代码如下:
services.AddAuthentication() .AddOAuth("LinkedIn", "LinkedIn", options => { var linkedinAuthNSection = Configuration.GetSection("Authentication:Linkedin"); options.ClientId = linkedinAuthNSection["ClientId"]; options.ClientSecret = linkedinAuthNSection["ClientSecret"]; options.CallbackPath = new PathString("/signin-linkedin"); options.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization"; options.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken"; options.UserInformationEndpoint = "https://api.linkedin.com/v2/userinfo"; // 仅保留必要的登录权限 options.Scope.Add("r_liteprofile"); options.Scope.Add("r_emailaddress"); // 映射LinkedIn用户字段到Claims options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); options.ClaimActions.MapJsonKey("urn:linkedin:picture", "picture"); // 适配LinkedIn v2 API的请求规则 options.Events = new OAuthEvents { OnCreatingTicket = async context => { var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted); response.EnsureSuccessStatusCode(); var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; context.RunClaimActions(user); } }; });
最后测试前记得清理浏览器缓存,避免之前的错误授权状态干扰新的请求。
内容的提问来源于stack exchange,提问作者Zubair Rana

