From f1e2f76c5425dcd69467eb7da523c616ffe837d9 Mon Sep 17 00:00:00 2001 From: vicenteyu Date: Wed, 24 Aug 2022 15:23:47 +0800 Subject: [PATCH] Add Feishu provider (#709) * Add Feishu provider Signed-off-by: Vicente Yu * Add Feishu provider. Signed-off-by: Vicente Yu --- AspNet.Security.OAuth.Providers.sln | 7 ++ README.md | 2 + .../AspNet.Security.OAuth.Feishu.csproj | 20 +++++ .../FeishuAuthenticationConstants.cs | 19 +++++ .../FeishuAuthenticationDefaults.cs | 48 ++++++++++++ .../FeishuAuthenticationExtensions.cs | 74 +++++++++++++++++++ .../FeishuAuthenticationHandler.cs | 71 ++++++++++++++++++ .../FeishuAuthenticationOptions.cs | 31 ++++++++ .../Feishu/FeishuTests.cs | 42 +++++++++++ .../Feishu/bundle.json | 39 ++++++++++ 10 files changed, 353 insertions(+) create mode 100644 src/AspNet.Security.OAuth.Feishu/AspNet.Security.OAuth.Feishu.csproj create mode 100644 src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationConstants.cs create mode 100644 src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationDefaults.cs create mode 100644 src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationExtensions.cs create mode 100644 src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationHandler.cs create mode 100644 src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationOptions.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Feishu/FeishuTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Feishu/bundle.json diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index 360147bbb..3ef048da1 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -283,6 +283,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.HubSp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Snapchat", "src\AspNet.Security.OAuth.Snapchat\AspNet.Security.OAuth.Snapchat.csproj", "{ECD22287-9B9F-489A-84A7-E66D65A39D73}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Feishu", "src\AspNet.Security.OAuth.Feishu\AspNet.Security.OAuth.Feishu.csproj", "{B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -645,6 +647,10 @@ Global {ECD22287-9B9F-489A-84A7-E66D65A39D73}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECD22287-9B9F-489A-84A7-E66D65A39D73}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECD22287-9B9F-489A-84A7-E66D65A39D73}.Release|Any CPU.Build.0 = Release|Any CPU + {B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -745,6 +751,7 @@ Global {289A91E9-81A9-422D-9CCD-12819081A29A} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {23E576EB-6514-4617-8F04-FE7D5540136D} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {ECD22287-9B9F-489A-84A7-E66D65A39D73} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {B8F9B052-84BF-436C-B22B-CEBD5EB1F8E3} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C} diff --git a/README.md b/README.md index 92febdfc3..6b1544c83 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ We would love it if you could help contributing to this repository. * [Yannic Smeets](https://github.com/yannicsmeets) * [zAfLu](https://github.com/zAfLu) * [zhengchun](https://github.com/zhengchun) +* [Vicente Yu](https://github.com/vicenteyu) * [Volodymyr Baydalka](https://github.com/zVolodymyr) * [Logan Dam](https://github.com/biltongza) @@ -141,6 +142,7 @@ If a provider you're looking for does not exist, consider making a PR to add one | eBay | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Ebay?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Ebay/ "Download AspNet.Security.OAuth.Ebay from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Ebay?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Ebay "Download AspNet.Security.OAuth.Ebay from MyGet.org") | [Documentation](https://developer.ebay.com/api-docs/static/oauth-tokens.html "eBay developer documentation") | | EVEOnline | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.EVEOnline?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.EVEOnline/ "Download AspNet.Security.OAuth.EVEOnline from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.EVEOnline?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.EVEOnline "Download AspNet.Security.OAuth.EVEOnline from MyGet.org") | [Documentation](https://github.com/esi/esi-docs/blob/master/docs/sso/web_based_sso_flow.md "EVEOnline developer documentation") | | ExactOnline | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.ExactOnline?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.ExactOnline/ "Download AspNet.Security.OAuth.ExactOnline from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.ExactOnline?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.ExactOnline "Download AspNet.Security.OAuth.ExactOnline from MyGet.org") | [Documentation](https://support.exactonline.com/community/s/knowledge-base#All-All-DNO-Content-gettingstarted "ExactOnline developer documentation") | +| Feishu | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Feishu?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Feishu/ "Download AspNet.Security.OAuth.Feishu from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Feishu?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Feishu "Download AspNet.Security.OAuth.Feishu from MyGet.org") | [Documentation](https://open.feishu.cn/document/common-capabilities/sso/web-application-sso/web-app-overview "Feishu developer documentation") | | Fitbit | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Fitbit?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Fitbit/ "Download AspNet.Security.OAuth.Fitbit from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Fitbit?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Fitbit "Download AspNet.Security.OAuth.Fitbit from MyGet.org") | [Documentation](https://dev.fitbit.com/build/reference/web-api/oauth2/ "Fitbit developer documentation") | | Foursquare | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Foursquare?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Foursquare/ "Download AspNet.Security.OAuth.Foursquare from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Foursquare?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Foursquare "Download AspNet.Security.OAuth.Foursquare from MyGet.org") | [Documentation](https://developer.foursquare.com/docs/api/configuration/authentication "Foursquare developer documentation") | | Gitee | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Gitee?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Gitee/ "Download AspNet.Security.OAuth.Gitee from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Gitee?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Gitee "Download AspNet.Security.OAuth.Gitee from MyGet.org") | [Documentation](https://gitee.com/api/v5/oauth_doc#/ "Gitee developer documentation") | diff --git a/src/AspNet.Security.OAuth.Feishu/AspNet.Security.OAuth.Feishu.csproj b/src/AspNet.Security.OAuth.Feishu/AspNet.Security.OAuth.Feishu.csproj new file mode 100644 index 000000000..fbc305de4 --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/AspNet.Security.OAuth.Feishu.csproj @@ -0,0 +1,20 @@ + + + + true + 6.0.9 + $(DefaultNetCoreTargetFramework) + + + + ASP.NET Core security middleware enabling Feishu authentication. + Vicente Yu + aspnetcore;authentication;feishu;oauth;security + + + + + + + + diff --git a/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationConstants.cs b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationConstants.cs new file mode 100644 index 000000000..977b7ad9c --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationConstants.cs @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Feishu; + +/// +/// Contains constants specific to the . +/// +public static class FeishuAuthenticationConstants +{ + public static class Claims + { + public const string UnionId = "urn:feishu:unionid"; + public const string Avatar = "urn:feishu:avatar"; + } +} diff --git a/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationDefaults.cs new file mode 100644 index 000000000..c2e70f588 --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationDefaults.cs @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Feishu; + +/// +/// Default values for Feishu authentication. +/// +public static class FeishuAuthenticationDefaults +{ + /// + /// Default value for . + /// + public const string AuthenticationScheme = "Feishu"; + + /// + /// Default value for . + /// + public static readonly string DisplayName = "Feishu"; + + /// + /// Default value for . + /// + public static readonly string CallbackPath = "/signin-feishu"; + + /// + /// Default value for . + /// + public static readonly string Issuer = "Feishu"; + + /// + /// Default value for . + /// + public static readonly string AuthorizationEndpoint = "https://passport.feishu.cn/suite/passport/oauth/authorize"; + + /// + /// Default value for . + /// + public static readonly string TokenEndpoint = "https://passport.feishu.cn/suite/passport/oauth/token"; + + /// + /// Default value for . + /// + public static readonly string UserInformationEndpoint = "https://passport.feishu.cn/suite/passport/oauth/userinfo"; +} diff --git a/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationExtensions.cs new file mode 100644 index 000000000..52f85f842 --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationExtensions.cs @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using AspNet.Security.OAuth.Feishu; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to add Feishu authentication capabilities to an HTTP application pipeline. +/// +public static class FeishuAuthenticationExtensions +{ + /// + /// Adds to the specified + /// , which enables Feishu authentication capabilities. + /// + /// The authentication builder. + /// The . + public static AuthenticationBuilder AddFeishu([NotNull] this AuthenticationBuilder builder) + { + return builder.AddFeishu(FeishuAuthenticationDefaults.AuthenticationScheme, _ => { }); + } + + /// + /// Adds to the specified + /// , which enables Feishu authentication capabilities. + /// + /// The authentication builder. + /// The delegate used to configure the OpenID 2.0 options. + /// The . + public static AuthenticationBuilder AddFeishu( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddFeishu(FeishuAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables Feishu authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the Feishu options. + /// The . + public static AuthenticationBuilder AddFeishu( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddFeishu(scheme, FeishuAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables Feishu authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The optional display name associated with this instance. + /// The delegate used to configure the Feishu options. + /// The . + public static AuthenticationBuilder AddFeishu( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [CanBeNull] string caption, + [NotNull] Action configuration) + { + return builder.AddOAuth(scheme, caption, configuration); + } +} diff --git a/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationHandler.cs new file mode 100644 index 000000000..6409eed7c --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationHandler.cs @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.Feishu; + +public partial class FeishuAuthenticationHandler : OAuthHandler +{ + public FeishuAuthenticationHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder, + [NotNull] ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override async Task CreateTicketAsync( + [NotNull] ClaimsIdentity identity, + [NotNull] AuthenticationProperties properties, + [NotNull] OAuthTokenResponse tokens) + { + using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + + using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted); + throw new HttpRequestException("An error occurred while retrieving the user profile."); + } + + using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); + + var principal = new ClaimsPrincipal(identity); + var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); + } + + private static partial class Log + { + internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + UserProfileError( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + + [LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + private static partial void UserProfileError( + ILogger logger, + System.Net.HttpStatusCode status, + string headers, + string body); + } +} diff --git a/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationOptions.cs new file mode 100644 index 000000000..c949017cb --- /dev/null +++ b/src/AspNet.Security.OAuth.Feishu/FeishuAuthenticationOptions.cs @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Security.Claims; +using static AspNet.Security.OAuth.Feishu.FeishuAuthenticationConstants; + +namespace AspNet.Security.OAuth.Feishu; + +/// +/// Defines a set of options used by . +/// +public class FeishuAuthenticationOptions : OAuthOptions +{ + public FeishuAuthenticationOptions() + { + ClaimsIssuer = FeishuAuthenticationDefaults.Issuer; + CallbackPath = FeishuAuthenticationDefaults.CallbackPath; + + AuthorizationEndpoint = FeishuAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = FeishuAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = FeishuAuthenticationDefaults.UserInformationEndpoint; + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "open_id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(Claims.UnionId, "union_id"); + ClaimActions.MapJsonKey(Claims.Avatar, "avatar_big"); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Feishu/FeishuTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Feishu/FeishuTests.cs new file mode 100644 index 000000000..b39115e7a --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Feishu/FeishuTests.cs @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Feishu; + +public class FeishuTests : OAuthTests +{ + public FeishuTests(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper; + } + + public override string DefaultScheme => FeishuAuthenticationDefaults.AuthenticationScheme; + + protected internal override void RegisterAuthentication(AuthenticationBuilder builder) + { + builder.AddFeishu(options => + { + ConfigureDefaults(builder, options); + }); + } + + [Theory] + [InlineData(ClaimTypes.NameIdentifier, "test-open-id")] + [InlineData(ClaimTypes.Name, "test-name")] + [InlineData(FeishuAuthenticationConstants.Claims.UnionId, "test-union-id")] + [InlineData(FeishuAuthenticationConstants.Claims.Avatar, "https://www.feishu.cn/avatar/icon_big")] + public async Task Can_Sign_In_Using_Feishu(string claimType, string claimValue) + { + // Arrange + using var server = CreateTestServer(); + + // Act + var claims = await AuthenticateUserAsync(server); + + // Assert + AssertClaim(claims, claimType, claimValue); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Feishu/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Feishu/bundle.json new file mode 100644 index 000000000..8a2763e2f --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Feishu/bundle.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "items": [ + { + "comment": "https://open.feishu.cn/document/common-capabilities/sso/api/get-access_token", + "uri": "https://passport.feishu.cn/suite/passport/oauth/token", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "access_token": "secret-access-token", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "secret-refresh-token", + "refresh_expires_in": 864000 + } + }, + { + "comment": "https://open.feishu.cn/document/common-capabilities/sso/api/get-user-info", + "uri": "https://passport.feishu.cn/suite/passport/oauth/userinfo", + "contentFormat": "json", + "contentJson": { + "sub": "ou_caecc734c2e3328a62489fe0648c4b98779515d3", + "name": "test-name", + "picture": "https://www.feishu.cn/avatar", + "open_id": "test-open-id", + "union_id": "test-union-id", + "en_name": "Lilei", + "tenant_key": "736588c92lxf175d", + "avatar_url": "https://www.feishu.cn/avatar/icon", + "avatar_thumb": "https://www.feishu.cn/avatar/icon_thumb", + "avatar_middle": "https://www.feishu.cn/avatar/icon_middle", + "avatar_big": "https://www.feishu.cn/avatar/icon_big", + "email": "zhangsan@feishu.cn", + "user_id": "5d9bdxxx", + "mobile": "+86130xxxx0000" + } + } + ] +}