Skip to content

Commit 8b3e741

Browse files
Add Zoho provider (#910)
Add provider for Zoho.
1 parent e252006 commit 8b3e741

File tree

9 files changed

+422
-0
lines changed

9 files changed

+422
-0
lines changed

AspNet.Security.OAuth.Providers.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Piped
309309
EndProject
310310
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Docusign", "src\AspNet.Security.OAuth.Docusign\AspNet.Security.OAuth.Docusign.csproj", "{4E96BD06-04CD-4014-BA42-10D2CDB820D6}"
311311
EndProject
312+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Zoho", "src\AspNet.Security.OAuth.Zoho\AspNet.Security.OAuth.Zoho.csproj", "{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}"
313+
EndProject
312314
Global
313315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
314316
Debug|Any CPU = Debug|Any CPU
@@ -715,6 +717,10 @@ Global
715717
{4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
716718
{4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
717719
{4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Release|Any CPU.Build.0 = Release|Any CPU
720+
{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
721+
{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
722+
{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
723+
{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Release|Any CPU.Build.0 = Release|Any CPU
718724
EndGlobalSection
719725
GlobalSection(SolutionProperties) = preSolution
720726
HideSolutionNode = FALSE
@@ -826,6 +832,7 @@ Global
826832
{83C37AC5-51FB-47CD-8CBE-77AA114FF6F3} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
827833
{55975423-C9C0-4C47-AD00-0F012F30AD3C} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
828834
{4E96BD06-04CD-4014-BA42-10D2CDB820D6} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
835+
{CD56ABE4-1CD2-4029-B556-E110A31A2CC4} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
829836
EndGlobalSection
830837
GlobalSection(ExtensibilityGlobals) = postSolution
831838
SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
251251
| Yandex | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Yandex?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Yandex/ "Download AspNet.Security.OAuth.Yandex from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Yandex?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Yandex "Download AspNet.Security.OAuth.Yandex from MyGet.org") | [Documentation](https://tech.yandex.com/oauth/ "Yandex developer documentation") |
252252
| Zalo | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Zalo?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zalo/ "Download AspNet.Security.OAuth.Zalo from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Zalo?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zalo "Download AspNet.Security.OAuth.Zalo from MyGet.org") | [Documentation](https://developers.zalo.me/docs/api/social-api-4 "Zalo developer documentation") |
253253
| Zendesk | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Zendesk?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zendesk/ "Download AspNet.Security.OAuth.Zendesk from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Zendesk?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zendesk "Download AspNet.Security.OAuth.Zendesk from MyGet.org") | [Documentation](https://support.zendesk.com/hc/en-us/articles/203663836#topic_ar1_mfs_qk "Zendesk developer documentation") |
254+
| Zoho | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Zoho?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zoho/ "Download AspNet.Security.OAuth.Zoho from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Zoho?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zoho "Download AspNet.Security.OAuth.Zoho from MyGet.org") | [Documentation](https://www.zoho.com/accounts/protocol/oauth.html "Zoho developer documentation") |
254255
| Zoom | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Zoom?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zoom/ "Download AspNet.Security.OAuth.Zoom from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Zoom?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zoom "Download AspNet.Security.OAuth.Zoom from MyGet.org") | [Documentation](https://developers.zoom.us/docs/integrations/ "Zoom developer documentation") |
255256

256257
<!--
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageValidationBaselineVersion>8.2.0</PackageValidationBaselineVersion>
5+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
6+
</PropertyGroup>
7+
8+
<!-- TODO Enable once this provider is published to NuGet.org -->
9+
<PropertyGroup>
10+
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
11+
</PropertyGroup>
12+
13+
<PropertyGroup>
14+
<Description>ASP.NET Core security middleware enabling Zoho authentication.</Description>
15+
<Authors>Denys Goncharenko</Authors>
16+
<PackageTags>aspnetcore;authentication;oauth;zoho;security</PackageTags>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
21+
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All"/>
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
namespace AspNet.Security.OAuth.Zoho;
8+
9+
/// <summary>
10+
/// Default values used by the Zoho authentication middleware.
11+
/// </summary>
12+
public static class ZohoAuthenticationDefaults
13+
{
14+
/// <summary>
15+
/// Default value for <see cref="AuthenticationScheme.Name"/>.
16+
/// </summary>
17+
public const string AuthenticationScheme = "Zoho";
18+
19+
/// <summary>
20+
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
21+
/// </summary>
22+
public static readonly string DisplayName = "Zoho";
23+
24+
/// <summary>
25+
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
26+
/// </summary>
27+
public static readonly string Issuer = "Zoho";
28+
29+
/// <summary>
30+
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
31+
/// </summary>
32+
public static readonly string CallbackPath = "/signin-zoho";
33+
34+
/// <summary>
35+
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
36+
/// </summary>
37+
public static readonly string AuthorizationEndpoint = "https://accounts.zoho.com/oauth/v2/auth";
38+
39+
/// <summary>
40+
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
41+
/// </summary>
42+
public static readonly string TokenPath = "/oauth/v2/token";
43+
44+
/// <summary>
45+
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
46+
/// </summary>
47+
public static readonly string TokenEndpoint = "https://accounts.zoho.com/oauth/v2/token";
48+
49+
/// <summary>
50+
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
51+
/// </summary>
52+
public static readonly string UserInformationPath = "/oauth/user/info";
53+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace AspNet.Security.OAuth.Zoho;
10+
11+
/// <summary>
12+
/// Extension methods to add Zoho authentication capabilities to an HTTP application pipeline.
13+
/// </summary>
14+
public static class ZohoAuthenticationExtensions
15+
{
16+
/// <summary>
17+
/// Adds <see cref="ZohoAuthenticationHandler"/> to the specified
18+
/// <see cref="AuthenticationBuilder"/>, which enables Zoho authentication capabilities.
19+
/// </summary>
20+
/// <param name="builder">The authentication builder.</param>
21+
/// <returns>A reference to this instance after the operation has completed.</returns>
22+
public static AuthenticationBuilder AddZoho([NotNull] this AuthenticationBuilder builder)
23+
{
24+
return builder.AddZoho(ZohoAuthenticationDefaults.AuthenticationScheme, options => { });
25+
}
26+
27+
/// <summary>
28+
/// Adds <see cref="ZohoAuthenticationHandler"/> to the specified
29+
/// <see cref="AuthenticationBuilder"/>, which enables Zoho authentication capabilities.
30+
/// </summary>
31+
/// <param name="builder">The authentication builder.</param>
32+
/// <param name="configuration">The delegate used to configure the OpenID 2.0 options.</param>
33+
/// <returns>A reference to this instance after the operation has completed.</returns>
34+
public static AuthenticationBuilder AddZoho(
35+
[NotNull] this AuthenticationBuilder builder,
36+
[NotNull] Action<ZohoAuthenticationOptions> configuration)
37+
{
38+
return builder.AddZoho(ZohoAuthenticationDefaults.AuthenticationScheme, configuration);
39+
}
40+
41+
/// <summary>
42+
/// Adds <see cref="ZohoAuthenticationHandler"/> to the specified
43+
/// <see cref="AuthenticationBuilder"/>, which enables Zoho authentication capabilities.
44+
/// </summary>
45+
/// <param name="builder">The authentication builder.</param>
46+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
47+
/// <param name="configuration">The delegate used to configure the Zoho options.</param>
48+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
49+
public static AuthenticationBuilder AddZoho(
50+
[NotNull] this AuthenticationBuilder builder,
51+
[NotNull] string scheme,
52+
[NotNull] Action<ZohoAuthenticationOptions> configuration)
53+
{
54+
return builder.AddZoho(scheme, ZohoAuthenticationDefaults.DisplayName, configuration);
55+
}
56+
57+
/// <summary>
58+
/// Adds <see cref="ZohoAuthenticationHandler"/> to the specified
59+
/// <see cref="AuthenticationBuilder"/>, which enables Zoho authentication capabilities.
60+
/// </summary>
61+
/// <param name="builder">The authentication builder.</param>
62+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
63+
/// <param name="caption">The optional display name associated with this instance.</param>
64+
/// <param name="configuration">The delegate used to configure the Zoho options.</param>
65+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
66+
public static AuthenticationBuilder AddZoho(
67+
[NotNull] this AuthenticationBuilder builder,
68+
[NotNull] string scheme,
69+
[CanBeNull] string caption,
70+
[NotNull] Action<ZohoAuthenticationOptions> configuration)
71+
{
72+
return builder.AddOAuth<ZohoAuthenticationOptions, ZohoAuthenticationHandler>(scheme, caption, configuration);
73+
}
74+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System.Net;
8+
using System.Net.Http.Headers;
9+
using System.Net.Mime;
10+
using System.Security.Claims;
11+
using System.Text.Encodings.Web;
12+
using System.Text.Json;
13+
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Options;
15+
16+
namespace AspNet.Security.OAuth.Zoho;
17+
18+
public partial class ZohoAuthenticationHandler : OAuthHandler<ZohoAuthenticationOptions>
19+
{
20+
public ZohoAuthenticationHandler(
21+
[NotNull] IOptionsMonitor<ZohoAuthenticationOptions> options,
22+
[NotNull] ILoggerFactory logger,
23+
[NotNull] UrlEncoder encoder)
24+
: base(options, logger, encoder)
25+
{
26+
}
27+
28+
protected override async Task<AuthenticationTicket> CreateTicketAsync(
29+
[NotNull] ClaimsIdentity identity,
30+
[NotNull] AuthenticationProperties properties,
31+
[NotNull] OAuthTokenResponse tokens)
32+
{
33+
var userInformationEndpoint = CreateEndpoint(ZohoAuthenticationDefaults.UserInformationPath);
34+
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInformationEndpoint);
35+
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
36+
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
37+
requestMessage.Version = Backchannel.DefaultRequestVersion;
38+
39+
using var response = await Backchannel.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
40+
if (!response.IsSuccessStatusCode)
41+
{
42+
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
43+
throw new HttpRequestException("An error occurred while retrieving the user profile.");
44+
}
45+
46+
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
47+
48+
var principal = new ClaimsPrincipal(identity);
49+
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
50+
context.RunClaimActions();
51+
52+
await Events.CreatingTicket(context);
53+
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
54+
}
55+
56+
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
57+
{
58+
var nameValueCollection = new Dictionary<string, string?>
59+
{
60+
["client_id"] = Options.ClientId,
61+
["client_secret"] = Options.ClientSecret,
62+
["code"] = context.Code,
63+
["redirect_uri"] = context.RedirectUri,
64+
["grant_type"] = "authorization_code"
65+
};
66+
67+
if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
68+
{
69+
nameValueCollection.Add(OAuthConstants.CodeVerifierKey, codeVerifier!);
70+
context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey);
71+
}
72+
73+
var tokenEndpoint = CreateEndpoint(ZohoAuthenticationDefaults.TokenPath);
74+
using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint);
75+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
76+
request.Content = new FormUrlEncodedContent(nameValueCollection);
77+
request.Version = Backchannel.DefaultRequestVersion;
78+
79+
using var response = await Backchannel.SendAsync(request, Context.RequestAborted);
80+
if (!response.IsSuccessStatusCode)
81+
{
82+
await Log.ExchangeCodeErrorAsync(Logger, response, Context.RequestAborted);
83+
return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token."));
84+
}
85+
86+
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
87+
88+
return OAuthTokenResponse.Success(payload);
89+
}
90+
91+
/// <summary>
92+
/// Creates the endpoint for the Zoho API using the location parameter.
93+
/// If the location parameter doesn't match any of the supported locations, the default location (US) is used.
94+
/// We don't use the <c>accounts-server</c> parameter for security reasons.
95+
/// </summary>
96+
/// <param name="path">The request path.</param>
97+
/// <returns>The API endpoint for the Zoho API.</returns>
98+
private string CreateEndpoint(string path)
99+
{
100+
var location = Context.Request.Query["location"];
101+
102+
var domain = location.ToString().ToLowerInvariant() switch
103+
{
104+
"au" => "https://accounts.zoho.com.au",
105+
"ca" => "https://accounts.zohocloud.ca",
106+
"eu" => "https://accounts.zoho.eu",
107+
"us" => "https://accounts.zoho.com",
108+
"in" => "https://accounts.zoho.in",
109+
"jp" => "https://accounts.zoho.jp",
110+
"sa" => "https://accounts.zoho.sa",
111+
"uk" => "https://accounts.zoho.uk",
112+
_ => "https://accounts.zoho.com"
113+
};
114+
115+
var builder = new UriBuilder(domain)
116+
{
117+
Path = path,
118+
Port = -1,
119+
Scheme = Uri.UriSchemeHttps,
120+
};
121+
122+
return builder.Uri.ToString();
123+
}
124+
125+
private static partial class Log
126+
{
127+
internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
128+
{
129+
UserProfileError(
130+
logger,
131+
response.StatusCode,
132+
response.Headers.ToString(),
133+
await response.Content.ReadAsStringAsync(cancellationToken));
134+
}
135+
136+
internal static async Task ServerInfoErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
137+
{
138+
ServerInfoErrorAsync(
139+
logger,
140+
response.StatusCode,
141+
response.Headers.ToString(),
142+
await response.Content.ReadAsStringAsync(cancellationToken));
143+
}
144+
145+
internal static async Task ExchangeCodeErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
146+
{
147+
ExchangeCodeError(
148+
logger,
149+
response.StatusCode,
150+
response.Headers.ToString(),
151+
await response.Content.ReadAsStringAsync(cancellationToken));
152+
}
153+
154+
[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}.")]
155+
private static partial void UserProfileError(
156+
ILogger logger,
157+
System.Net.HttpStatusCode status,
158+
string headers,
159+
string body);
160+
161+
[LoggerMessage(2, LogLevel.Error, "An error occurred while retrieving the server info: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")]
162+
private static partial void ServerInfoErrorAsync(
163+
ILogger logger,
164+
HttpStatusCode status,
165+
string headers,
166+
string body);
167+
168+
[LoggerMessage(3, LogLevel.Error, "An error occurred while retrieving an access token: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")]
169+
private static partial void ExchangeCodeError(
170+
ILogger logger,
171+
HttpStatusCode status,
172+
string headers,
173+
string body);
174+
}
175+
}

0 commit comments

Comments
 (0)