Skip to content

Commit

Permalink
Implement Pipedrive OAuth provider (#892)
Browse files Browse the repository at this point in the history
Implement Pipedrive OAuth provider.
  • Loading branch information
denis-goncharenko authored Jun 14, 2024
1 parent 22fe33a commit 3be27ad
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 0 deletions.
7 changes: 7 additions & 0 deletions AspNet.Security.OAuth.Providers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.PingO
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.JumpCloud", "src\AspNet.Security.OAuth.JumpCloud\AspNet.Security.OAuth.JumpCloud.csproj", "{8AF5DDBE-2631-4E71-9045-73A6356CE86B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Pipedrive", "src\AspNet.Security.OAuth.Pipedrive\AspNet.Security.OAuth.Pipedrive.csproj", "{55975423-C9C0-4C47-AD00-0F012F30AD3C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -678,6 +680,10 @@ Global
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.Build.0 = Release|Any CPU
{55975423-C9C0-4C47-AD00-0F012F30AD3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55975423-C9C0-4C47-AD00-0F012F30AD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55975423-C9C0-4C47-AD00-0F012F30AD3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55975423-C9C0-4C47-AD00-0F012F30AD3C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -783,6 +789,7 @@ Global
{101681FB-569F-4941-B943-2AD380039BE0} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{CF8C4235-6AE6-404E-B572-4FF4E85AB5FF} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{8AF5DDBE-2631-4E71-9045-73A6356CE86B} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
{55975423-C9C0-4C47-AD00-0F012F30AD3C} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
| Patreon | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Patreon?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Patreon/ "Download AspNet.Security.OAuth.Patreon from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Patreon?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Patreon "Download AspNet.Security.OAuth.Patreon from MyGet.org") | [Documentation](https://docs.patreon.com/#oauth "Patreon developer documentation") |
| Paypal | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Paypal?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Paypal/ "Download AspNet.Security.OAuth.Paypal from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Paypal?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Paypal "Download AspNet.Security.OAuth.Paypal from MyGet.org") | [Documentation](https://developer.paypal.com/docs/api-basics/#oauth-20-authorization-protocol "Paypal developer documentation") |
| PingOne | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.PingOne?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.PingOne/ "Download AspNet.Security.OAuth.PingOne from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.PingOne?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.PingOne "Download AspNet.Security.OAuth.PingOne from MyGet.org") | [Documentation](https://apidocs.pingidentity.com/pingone/platform/v1/api/#openid-connectoauth-2 "PingOne developer documentation") |
| Pipedrive | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Pipedrive?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Pipedrive/ "Download AspNet.Security.OAuth.Pipedrive from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Pipedrive?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Pipedrive "Download AspNet.Security.OAuth.Pipedrive from MyGet.org") | [Documentation](https://pipedrive.readme.io/docs/marketplace-oauth-authorization "Pipedrive developer documentation") |
| QQ | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.QQ?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.QQ/ "Download AspNet.Security.OAuth.QQ from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.QQ?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.QQ "Download AspNet.Security.OAuth.QQ from MyGet.org") | [Documentation](https://developers.e.qq.com/docs/apilist/auth/oauth2 "QQ developer documentation") |
| QuickBooks | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.QuickBooks?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.QuickBooks/ "Download AspNet.Security.OAuth.QuickBooks from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.QuickBooks?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.QuickBooks "Download AspNet.Security.OAuth.QuickBooks from MyGet.org") | [Documentation](https://www.developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0-playground "QuickBooks developer documentation") |
| Reddit | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Reddit?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Reddit/ "Download AspNet.Security.OAuth.Reddit from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Reddit?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Reddit "Download AspNet.Security.OAuth.Reddit from MyGet.org") | [Documentation](https://github.com/reddit-archive/reddit/wiki/oauth2 "Reddit developer documentation") |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFramework)</TargetFrameworks>
</PropertyGroup>

<!-- TODO Enable once this provider is published to NuGet.org -->
<PropertyGroup>
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
<PackageValidationBaselineVersion>8.0.1</PackageValidationBaselineVersion>
</PropertyGroup>

<PropertyGroup>
<Description>ASP.NET Core security middleware enabling Pipedrive authentication.</Description>
<Authors>Denys Goncharenko</Authors>
<PackageTags>aspnetcore;authentication;oauth;pipedrive;security</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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.Pipedrive;

/// <summary>
/// Default values used by the Pipedrive authentication middleware.
/// </summary>
public static class PipedriveAuthenticationDefaults
{
/// <summary>
/// Default value for <see cref="AuthenticationScheme.Name"/>.
/// </summary>
public const string AuthenticationScheme = "Pipedrive";

/// <summary>
/// Default value for <see cref="AuthenticationScheme.DisplayName"/>.
/// </summary>
public static readonly string DisplayName = "Pipedrive";

/// <summary>
/// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
/// </summary>
public static readonly string Issuer = "Pipedrive";

/// <summary>
/// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
/// </summary>
public static readonly string CallbackPath = "/signin-pipedrive";

/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
public static readonly string AuthorizationEndpoint = "https://oauth.pipedrive.com/oauth/authorize";

/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
public static readonly string TokenEndpoint = "https://oauth.pipedrive.com/oauth/token";

/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
public static readonly string UserInformationEndpoint = "https://api.pipedrive.com/v1/users/me";
}
Original file line number Diff line number Diff line change
@@ -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 Microsoft.Extensions.DependencyInjection;

namespace AspNet.Security.OAuth.Pipedrive;

/// <summary>
/// Extension methods to add Pipedrive authentication capabilities to an HTTP application pipeline.
/// </summary>
public static class PipedriveAuthenticationExtensions
{
/// <summary>
/// Adds <see cref="PipedriveAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Pipedrive authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthenticationBuilder AddPipedrive([NotNull] this AuthenticationBuilder builder)
{
return builder.AddPipedrive(PipedriveAuthenticationDefaults.AuthenticationScheme, options => { });
}

/// <summary>
/// Adds <see cref="PipedriveAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Pipedrive authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="configuration">The delegate used to configure the OpenID 2.0 options.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthenticationBuilder AddPipedrive(
[NotNull] this AuthenticationBuilder builder,
[NotNull] Action<PipedriveAuthenticationOptions> configuration)
{
return builder.AddPipedrive(PipedriveAuthenticationDefaults.AuthenticationScheme, configuration);
}

/// <summary>
/// Adds <see cref="PipedriveAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Pipedrive authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Pipedrive options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddPipedrive(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[NotNull] Action<PipedriveAuthenticationOptions> configuration)
{
return builder.AddPipedrive(scheme, PipedriveAuthenticationDefaults.DisplayName, configuration);
}

/// <summary>
/// Adds <see cref="PipedriveAuthenticationHandler"/> to the specified
/// <see cref="AuthenticationBuilder"/>, which enables Pipedrive authentication capabilities.
/// </summary>
/// <param name="builder">The authentication builder.</param>
/// <param name="scheme">The authentication scheme associated with this instance.</param>
/// <param name="caption">The optional display name associated with this instance.</param>
/// <param name="configuration">The delegate used to configure the Pipedrive options.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddPipedrive(
[NotNull] this AuthenticationBuilder builder,
[NotNull] string scheme,
[CanBeNull] string caption,
[NotNull] Action<PipedriveAuthenticationOptions> configuration)
{
return builder.AddOAuth<PipedriveAuthenticationOptions, PipedriveAuthenticationHandler>(scheme, caption, configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.Pipedrive;

public partial class PipedriveAuthenticationHandler : OAuthHandler<PipedriveAuthenticationOptions>
{
public PipedriveAuthenticationHandler(
[NotNull] IOptionsMonitor<PipedriveAuthenticationOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override async Task<AuthenticationTicket> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

namespace AspNet.Security.OAuth.Pipedrive;

/// <summary>
/// Defines a set of options used by <see cref="PipedriveAuthenticationHandler"/>.
/// </summary>
public class PipedriveAuthenticationOptions : OAuthOptions
{
public PipedriveAuthenticationOptions()
{
ClaimsIssuer = PipedriveAuthenticationDefaults.Issuer;
CallbackPath = PipedriveAuthenticationDefaults.CallbackPath;

AuthorizationEndpoint = PipedriveAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = PipedriveAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = PipedriveAuthenticationDefaults.UserInformationEndpoint;

Scope.Add("base");

ClaimActions.MapCustomJson(ClaimTypes.NameIdentifier, user => user.GetProperty("data").GetString("id"));
ClaimActions.MapCustomJson(ClaimTypes.Name, user => user.GetProperty("data").GetString("name"));
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.GetProperty("data").GetString("email"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.Pipedrive;

public class PipedriveTests(ITestOutputHelper outputHelper) : OAuthTests<PipedriveAuthenticationOptions>(outputHelper)
{
public override string DefaultScheme => PipedriveAuthenticationDefaults.AuthenticationScheme;

protected internal override void RegisterAuthentication(AuthenticationBuilder builder)
{
builder.AddPipedrive(options => ConfigureDefaults(builder, options));
}

[Theory]
[InlineData(ClaimTypes.NameIdentifier, "12345")]
[InlineData(ClaimTypes.Name, "Test User")]
[InlineData(ClaimTypes.Email, "testuser@example.com")]
public async Task Can_Sign_In_Using_Pipedrive(string claimType, string claimValue)
=> await AuthenticateUserAndAssertClaimValue(claimType, claimValue);
}
28 changes: 28 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Pipedrive/bundle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
"items": [
{
"uri": "https://oauth.pipedrive.com/oauth/token",
"method": "POST",
"contentFormat": "json",
"contentJson": {
"access_token": "secret-access-token",
"token_type": "access",
"refresh_token": "secret-refresh-token",
"expires_in": "300"
}
},
{
"comment": "https://pipedrive.readme.io/docs/marketplace-getting-user-data",
"uri": "https://api.pipedrive.com/v1/users/me",
"contentFormat": "json",
"contentJson": {
"data": {
"id": "12345",
"name": "Test User",
"email": "testuser@example.com"
}
}
}
]
}

0 comments on commit 3be27ad

Please sign in to comment.