卩伪装

netcore苹果登陆后端验证Sign In With Apple

  2020年4月之后,上架App Store得应用必须集成apple账号得登录。

  近期博主刚好配合前端IOS集成apple登录,网上找了不少文章教程,发现基本都是网页集成登录或者是java代码,比较少纯后端net验证,期间也走了不少弯路,在这分享给大家实现思路和需要得注意事项。

  文章开始前先说明一下此文环境为netcore3.1环境代码编写,IOS相关配置和文章请参考文末链接。

  整体思路为:前端调用苹果接口获取到userID和authorizationCode,后端通过authorizationCode调用苹果接口验证,若检验成功会返回相关信息;

  以下为apple官方接口文档说明:https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

  根据文档可知,调用授权码验证需要传递6个参数,其中3个必填client_idclient_secretgrant_type

  client_idclient_secret为apple验证请求方是否合法,client_secret得生成也本文得重点讲解。

  grant_type  我理解为操作方式,固定为验证授权码authorization_code和刷新token refresh_token,因为我们是验证授权码,所以只需传递authorization_code

  这里注意Content-Type: application/x-www-form-urlencoded,若使用postman工具尝试请求Body请记得选x-www-form-urlencoded,否则请求格式失败必定“invalid_client”;

  先说请求的几种错误的返回格式:

  

返回 原因
invalid_client client_id或client_secret错误,请复制下面代码生成
invalid_grant
authorization_code 授权码错误,可以去怼前端了
unsupported_grant_type
grant_type错误,嗯请固定authorization_code,别问我为什么知道,当然是特意去请求尝试给你们看的啦

 

  

说了这么多先贴一下请求代码:

只要成功返回,解析返回值的IdToken,jwt解析第二段验证Aud和clientId,Sub与userID一致即可:

 1 /// <summary>
 2         /// 检验生成的授权码是正确的,需要给出正确的授权码
 3         /// </summary>
 4         /// <param name="authorizationCode">授权码</param>
 5         /// <param name="appUserId">apple用户ID</param>
 6         /// <returns></returns>
 7         public async Task TestAppleSign(string authorizationCode, string appUserId)
 8         {
 9             var httpClientHandler = new HttpClientHandler
10             {
11                 ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true
12             };
13             var httpClient = new HttpClient(httpClientHandler, true)
14             {
15                 //超时时间设置长一点点,有时候响应超过3秒,根据情况设置
16                 //我这里是单元测试,防止异常所以写30秒
17                 Timeout = TimeSpan.FromSeconds(30)
18             };
19             var newToken = CreateNewClientSecret();
20             var clientId = "bundleId";//找IOS要 
21             var datas = new Dictionary<string, string>()
22             {
23                 {"client_id", clientId},  
24                 {"grant_type", "authorization_code"},//固定authorization_code
25                 {"code",authorizationCode },//授权码,前端验证登录给予 
26                 {"client_secret",newToken} //client_secret,后面方法生成
27             };
28             //x-www-form-urlencoded 使用FormUrlEncodedContent
29             var formdata = new FormUrlEncodedContent(datas);
30             var result = await httpClient.PostAsync("https://appleid.apple.com/auth/token", formdata);
31             var re = await result.Content.ReadAsStringAsync();
32             if (result.IsSuccessStatusCode)
33             {
34                 var deserializeObject = JsonConvert.DeserializeObject<TokenResult>(re);
35                 var jwtPlayload = DecodeJwtPlayload(deserializeObject.IdToken);
36                 if (!jwtPlayload.Aud.Equals(clientId) || !jwtPlayload.Sub.Equals(appUserId))//appUserId,前端验证登录给予 
37                 {
38 
39                 }
40             }
41             else
42             {
43                 //请根据re的返回值,查看上面的错误表格
44             }
45         }

 

 

 

  生成ClientSecret代码如下:需要注意的是IssuedAtNotBefore时间需要留些余地,我就是在windos开发成功,部署到服务器环境后一致报invalid_client,之前发生过加密错误,还以为又是加密方式导致,经过一段时间的排除后发现是时间的坑

 1       public static string CreateNewClientSecret()
 2         {
 3             var handler = new JwtSecurityTokenHandler();
 4             var subject = new Claim("sub", "bundleId");//找IOS要 
 5             var tokenDescriptor = new SecurityTokenDescriptor()
 6             {
 7                 Audience = "https://appleid.apple.com",//固定值
 8                 Expires = DateTime.Now.AddMonths(5),//ClientSecret超时时间,可以设置长一点,也可以根据需要设置有效时间
 9                 Issuer = "team ID",//team ID,找IOS要
10                 //防止服务器时间比apple时间晚
11                 //签发时间
12                 IssuedAt = DateTime.Now.AddDays(-1),
13                 //防止服务器时间比apple时间晚
14                 //生效时间
15                 NotBefore = DateTime.Now.AddDays(-1),
16                 Subject = new ClaimsIdentity(new[] { subject }),
17             };
18             byte[] keyBlob = GetPrivateKeyBytesAsync();
19             var algorithm = CreateAlgorithm(keyBlob);
20             {
21                 tokenDescriptor.SigningCredentials = CreateSigningCredentials("KeyID", algorithm);//p8私钥文件得Key,找IOS要
22 
23                 var clientSecret = handler.CreateEncodedJwt(tokenDescriptor);
24 
25                 return clientSecret;
26             }
27         }
28         public static byte[] GetPrivateKeyBytesAsync()
29         {
30             //p8文件内容
31             string content = @"-----BEGIN PRIVATE KEY-----
32             ********************************
33             *******************************
34             ***********************************
35             *************
36                 ---- - END -----";
37 
38             if (content.StartsWith("-----BEGIN PRIVATE KEY-----", StringComparison.Ordinal))
39             {
40                 string[] keyLines = content.Split('\n');
41                 content = string.Join(string.Empty, keyLines.Skip(1).Take(keyLines.Length - 2));
42             }
43             
44             return Convert.FromBase64String(content);
45         }
46         private static ECDsa CreateAlgorithm(byte[] keyBlob)
47         {
48             var algorithm = ECDsa.Create();
49 
50             try
51             {
52                 algorithm.ImportPkcs8PrivateKey(keyBlob, out int _);
53                 return algorithm;
54             }
55             catch (Exception)
56             {
57                 algorithm?.Dispose();
58                 throw;
59             }
60         }
61         private static SigningCredentials CreateSigningCredentials(string keyId, ECDsa algorithm)
62         {
63             var key = new ECDsaSecurityKey(algorithm) { KeyId = keyId };
64             return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256Signature);
65         }

只要成功返回,解析返回值的IdToken,jwt解析第二段验证AudclientIdSubuserID一致即可,附反序列化返回值和jwt解析:

  

  1  /// <summary>
  2         /// 解析jwt第二部分
  3         /// </summary>
  4         /// <param name="jwtString"></param>
  5         /// <returns></returns>
  6         private JwtPlayload DecodeJwtPlayload(string jwtString)
  7         {
  8             try
  9             {
 10                 var code = jwtString.Split('.')[1];
 11                 code = code.Replace('-', '+').Replace('_', '/').PadRight(4 * ((code.Length + 3) / 4), '=');
 12                 var bytes = Convert.FromBase64String(code);
 13                 var decode = Encoding.UTF8.GetString(bytes);
 14                 return JsonConvert.DeserializeObject<JwtPlayload>(decode);
 15             }
 16             catch (Exception e)
 17             {
 18                 throw new Exception(e.Message);
 19             }
 20         }
 21         /// <summary>
 22         /// 接口返回值
 23         /// </summary>
 24         public class TokenResult
 25         {
 26             /// <summary>
 27             /// 一个token
 28             /// </summary>
 29             [JsonProperty("access_token")]
 30             public string AccessToken { get; set; }
 31             /// <summary>
 32             /// Bearer
 33             /// </summary>
 34             [JsonProperty("token_type")]
 35             public string TokenType { get; set; }
 36             /// <summary>
 37             /// 
 38             /// </summary>
 39             [JsonProperty("expires_in")]
 40             public long ExpiresIn { get; set; }
 41             /// <summary>
 42             /// 一个token
 43             /// </summary>
 44             [JsonProperty("refresh_token")]
 45             public string RefreshToken { get; set; }
 46             /// <summary>
 47             /// "结果是JWT,字符串形式,identityToken 解析后和客户端端做比对
 48             /// </summary>
 49             [JsonProperty("id_token")]
 50             public string IdToken { get; set; }
 51         }
 52         /// <summary>
 53         /// jwt第二部分
 54         /// </summary>
 55         private class JwtPlayload
 56         {
 57             /// <summary>
 58             /// "https://appleid.apple.com"
 59             /// </summary>
 60             [JsonProperty("iss")]
 61             public string Iss { get; set; }
 62             /// <summary>
 63             /// 这个是你的app的bundle identifier
 64             /// </summary>
 65             [JsonProperty("aud")]
 66             public string Aud { get; set; }
 67             /// <summary>
 68             /// 
 69             /// </summary>
 70             [JsonProperty("exp")]
 71             public long Exp { get; set; }
 72             /// <summary>
 73             /// 
 74             /// </summary>
 75             [JsonProperty("iat")]
 76             public long Iat { get; set; }
 77             /// <summary>
 78             /// 用户ID
 79             /// </summary>
 80             [JsonProperty("sub")]
 81             public string Sub { get; set; }
 82             /// <summary>
 83             /// 
 84             /// </summary>
 85             [JsonProperty("at_hash")]
 86             public string AtHash { get; set; }
 87             /// <summary>
 88             /// 
 89             /// </summary>
 90             [JsonProperty("email")]
 91             public string Email { get; set; }
 92             /// <summary>
 93             /// 
 94             /// </summary>
 95             [JsonProperty("email_verified")]
 96             public bool EmailVerified { get; set; }
 97             /// <summary>
 98             /// 
 99             /// </summary>
100             [JsonProperty("is_private_email")]
101             public bool IsPrivateEmail { get; set; }
102             /// <summary>
103             /// 
104             /// </summary>
105             [JsonProperty("auth_time")]
106             public long AuthTime { get; set; }
107             /// <summary>
108             /// 
109             /// </summary>
110             [JsonProperty("nonce_supported")]
111             public bool NonceSupported { get; set; }
112         }
113         

 

 

 

 

  下面得生成client_secret方式是由文章参考而来:https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core

  但是这个方式只适用于windos,使用容器方式部署到linux时发现CnKey加密方式会报平台错误,若只在windos部署可直接使用该方式

public static string CreateNewToken()
    {
        const string iss = "62QM29578N"; // your account's team ID found in the dev portal
        const string aud = "https://appleid.apple.com";
        const string sub = "com.scottbrady91.authdemo.service"; // same as client_id
        const string privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgnbfHJQO9feC7yKOenScNctvHUP+Hp3AdOKnjUC3Ee9GgCgYIKoZIzj0DAQehRANCAATMgckuqQ1MhKALhLT/CA9lZrLA+VqTW/iIJ9GKimtC2GP02hCc5Vac8WuN6YjynF3JPWKTYjg2zqex5Sdn9Wj+"; // contents of .p8 file
        
        var cngKey = CngKey.Import(
          Convert.FromBase64String(privateKey), 
          CngKeyBlobFormat.Pkcs8PrivateBlob);
        
        var handler = new JwtSecurityTokenHandler();
        var token = handler.CreateJwtSecurityToken(
            issuer: iss,
            audience: aud,
            subject: new ClaimsIdentity(new List<Claim> {new Claim("sub", sub)}),
            expires: DateTime.UtcNow.AddMinutes(5), // expiry can be a maximum of 6 months
            issuedAt: DateTime.UtcNow,
            notBefore: DateTime.UtcNow,
            signingCredentials: new SigningCredentials(
              new ECDsaSecurityKey(new ECDsaCng(cngKey)), SecurityAlgorithms.EcdsaSha256));

        return handler.WriteToken(token);
    }

 

2021.1.12 经12楼同学反馈

redirect_uri可以为空, 我没传递redirect_uri参数可以正常认证,他没传递会导致invalid_client,所以他就随意传递了一个值导致正常的授权码一直返回invalid_grant,补充一下给其他出错的同学参考。

 

 

参考资料:

IOS授权全教程:https://blog.csdn.net/WangErice/article/details/104823811?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-11&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-11

         https://www.jianshu.com/p/e1284bd8c72a

苹果授权登陆后端验证:

      https://blog.csdn.net/wpf199402076118/article/details/99677412

netcore验证相关文章:

      https://www.scottbrady91.com/OpenID-Connect/Implementing-Sign-In-with-Apple-in-ASPNET-Core

      https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers

posted on 2020-04-14 23:11  卩伪装  阅读(1451)  评论(17编辑  收藏  举报

导航