Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Zendesk #2217

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

using System.Collections.Immutable;
using System.Net.Http;
using System.Net.Http.Headers;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.UserInfo;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;

namespace OpenIddict.Client.WebIntegration;
Expand All @@ -17,12 +19,99 @@ public static partial class OpenIddictClientWebIntegrationHandlers
public static class Revocation
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([

/*
* Revocation request preparation:
*/
OverrideHttpMethod.Descriptor,
AttachAccessTokenParameter.Descriptor,
/*
* Revocation response extraction:
*/
NormalizeContentType.Descriptor
]);

/// <summary>
/// Contains the logic responsible for overriding the HTTP method for the providers that require it.
/// </summary>
public sealed class OverrideHttpMethod : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<OverrideHttpMethod>()
.SetOrder(PreparePostHttpRequest<PrepareRevocationRequestContext>.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

/// <inheritdoc/>
public ValueTask HandleAsync(PrepareRevocationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack.
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));

request.Method = context.Registration.ProviderType switch
{

ProviderTypes.Zendesk => HttpMethod.Delete,

_ => request.Method
};

return default;
}
}

/// <summary>
/// Contains the logic responsible for attaching the access token
/// parameter to the request for the providers that require it.
/// </summary>
public sealed class AttachAccessTokenParameter : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachAccessTokenParameter>()
.SetOrder(AttachBearerAccessToken.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

/// <inheritdoc/>
public ValueTask HandleAsync(PrepareRevocationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack.
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));

// Zendesk requires using the token that is going to be revoked
if (context.Registration.ProviderType is ProviderTypes.Zendesk)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.Token);
}

return default;
}
}

/// <summary>
/// Contains the logic responsible for normalizing the returned content
/// type of revocation responses for the providers that require it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,9 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)

// Shopify returns the email address as a custom "associated_user/email" node in token responses:
ProviderTypes.Shopify => (string?) context.TokenResponse?["associated_user"]?["email"],

// Zendesk returns the emails address as a custom "user/email" node:
ProviderTypes.Zendesk => (string?) context.UserInfoResponse?["user"]?["email"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the userinfo details are wrapped in a user node? If so, we'll need to unwrap the userinfo response so all the claims can be correctly mapped: https://documentation.openiddict.com/guides/contributing-a-new-web-provider#unwrap-userinfo-responses-if-necessary

Once done, you'll need to remove the ?["user"] part of these new lines.


_ => context.MergedPrincipal.GetClaim(ClaimTypes.Email)
});
Expand Down Expand Up @@ -1441,6 +1444,9 @@ ProviderTypes.Spotify or ProviderTypes.StackExchange or ProviderTypes.Zoom
// Typeform returns the username as a custom "alias" node:
ProviderTypes.Typeform => (string?) context.UserInfoResponse?["alias"],

// Zendesk returns the username as a custom "user/name" node:
ProviderTypes.Zendesk => (string?) context.UserInfoResponse?["user"]?["name"],

// Zoho returns the username as a custom "Display_Name" node:
ProviderTypes.Zoho => (string?) context.UserInfoResponse?["Display_Name"],

Expand Down Expand Up @@ -1529,6 +1535,9 @@ ProviderTypes.Twitter or ProviderTypes.Weibo or ProviderTypes.Zoom
// WordPress returns the user identifier as a custom "ID" node:
ProviderTypes.WordPress => (string?) context.UserInfoResponse?["ID"],

// Zendesk returns the user identifier as a custom "user/id" node:
ProviderTypes.Zendesk => (string?) context.UserInfoResponse?["user"]?["id"],

// WordPress returns the user identifier as a custom "ZUID" node:
ProviderTypes.Zoho => (string?) context.UserInfoResponse?["ZUID"],

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,23 @@
<Environment Issuer="https://api.login.yahoo.com/" />
</Provider>

<Provider Name="Zendesk" Id="5577b117-1f3d-464f-b987-aabf7c0ab3f7" Documentation="https://support.zendesk.com/hc/en-us/articles/4408845965210-Using-OAuth-authentication-with-your-application">

<!--Note: Zendesk is a multitenant provider that relies on subdomains to identify instances.
As such, the following URLs all include a {settings.Tenant} placeholder that will be dynamically
replaced by OpenIddict at runtime by the tenant configured in the Zendesk settings.-->

<Environment Issuer="https://{settings.Tenant}.zendesk.com/">
<Configuration AuthorizationEndpoint="https://{settings.Tenant}.zendesk.com/oauth/authorizations/new"
TokenEndpoint="https://{settings.Tenant}.zendesk.com/oauth/tokens"
UserInfoEndpoint="https://{settings.Tenant}.zendesk.com/api/v2/users/me"
RevocationEndpoint="https://{settings.Tenant}.zendesk.com/api/v2/oauth/tokens/current.json">
<CodeChallengeMethod Value="S256" />
</Configuration>
</Environment>
<Setting PropertyName="Tenant" ParameterName="tenant" Type="String" Required="true" Description="Gets or sets the tenant used to identify the Zendesk instance" />
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ▄▄▄ ██ ▄▄▄ ██ ██ ██ ▄▄▄ ██
Expand Down