From 5c5ab5cda46ca524f49cf225907b6f9165c1b055 Mon Sep 17 00:00:00 2001 From: Jerrie Pelser Date: Fri, 28 Mar 2025 14:08:35 +0200 Subject: [PATCH] Add Linear, Miro and Webflow to the list of supported providers --- ...ClientWebIntegrationHandlers.Revocation.cs | 90 ++++++++++++++++++- ...ctClientWebIntegrationHandlers.Userinfo.cs | 16 +++- .../OpenIddictClientWebIntegrationHandlers.cs | 37 ++++++-- ...penIddictClientWebIntegrationProviders.xml | 70 +++++++++++++++ 4 files changed, 200 insertions(+), 13 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs index a36d8142..29b42f35 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs @@ -5,8 +5,12 @@ */ using System.Collections.Immutable; +using System.Diagnostics; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Http.Json; +using OpenIddict.Client.SystemNetHttp; +using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; @@ -24,12 +28,61 @@ public static partial class OpenIddictClientWebIntegrationHandlers MapNonStandardRequestParameters.Descriptor, OverrideHttpMethod.Descriptor, AttachBearerAccessToken.Descriptor, + AttachNonStandardRequestPayload.Descriptor, /* * Revocation response extraction: */ NormalizeContentType.Descriptor ]); + + /// + /// Contains the logic responsible for attaching a non-standard payload for the providers that require it. + /// + public sealed class AttachNonStandardRequestPayload : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachHttpParameters.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(PrepareRevocationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); + + // 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.Content = context.Registration.ProviderType switch + { + // The token revocation endpoints exposed by these providers + // requires sending the request parameters as a JSON payload: + ProviderTypes.Miro => JsonContent.Create(context.Transaction.Request, + new MediaTypeHeaderValue(MediaTypes.Json) + { + CharSet = Charsets.Utf8 + }), + + _ => request.Content + }; + + return default; + } + } /// /// Contains the logic responsible for mapping non-standard request parameters @@ -55,15 +108,39 @@ public static partial class OpenIddictClientWebIntegrationHandlers throw new ArgumentNullException(nameof(context)); } - // Weibo, VK ID and Yandex don't support the standard "token" parameter and + // These providers don't support the standard "token" parameter and // require using the non-standard "access_token" parameter instead. - if (context.Registration.ProviderType is ProviderTypes.Weibo or ProviderTypes.VkId or ProviderTypes.Yandex) + if (context.Registration.ProviderType is + ProviderTypes.VkId or ProviderTypes.Webflow or + ProviderTypes.Weibo or ProviderTypes.Yandex) { context.Request.AccessToken = context.Token; context.Request.Token = null; context.Request.TokenTypeHint = null; } + // Linear requires only the access_token and no other parameters. + else if (context.Registration.ProviderType is ProviderTypes.Linear) + { + context.Request.AccessToken = context.Token; + context.Request.Token = null; + context.Request.TokenTypeHint = null; + context.Request.ClientId = null; + } + + // Miro uses a JSON payload that expects the non-standard + // "accessToken", "clientId" and "clientSecret" properties. + else if (context.Registration.ProviderType is ProviderTypes.Miro) + { + context.Request["accessToken"] = context.Token; + context.Request["clientId"] = context.Request.ClientId; + context.Request["clientSecret"] = context.Request.ClientSecret; + context.Request.Token = null; + context.Request.TokenTypeHint = null; + context.Request.ClientId = null; + context.Request.ClientSecret = null; + } + return default; } } @@ -147,6 +224,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Request.Token = null; } + // Miro requires using bearer authentication with the token that is going to be revoked. + // + // Note: the token property CANNOT be used here as the token parameter is mapped to "accessToken". + else if (context.Registration.ProviderType is ProviderTypes.Miro && + (string?) context.Request["accessToken"] is { Length: > 0 } token) + { + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, token); + } + return default; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 9ba1ce40..5831dd21 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -72,7 +72,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers { // The userinfo endpoints exposed by these providers // are based on GraphQL, which requires using POST: - ProviderTypes.Meetup or ProviderTypes.SubscribeStar => HttpMethod.Post, + ProviderTypes.Linear or ProviderTypes.Meetup or ProviderTypes.SubscribeStar => HttpMethod.Post, // The userinfo endpoints exposed by these providers // use custom protocols that require using POST: @@ -281,7 +281,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers { // The userinfo endpoints exposed by these providers are based on GraphQL, // which requires sending the request parameters as a JSON payload: - ProviderTypes.Meetup or ProviderTypes.SubscribeStar + ProviderTypes.Linear or ProviderTypes.Meetup or ProviderTypes.SubscribeStar => JsonContent.Create(context.Transaction.Request, new MediaTypeHeaderValue(MediaTypes.Json) { CharSet = Charsets.Utf8 @@ -430,10 +430,22 @@ public static partial class OpenIddictClientWebIntegrationHandlers => new(context.Response["data"]?.GetNamedParameters() ?? throw new InvalidOperationException(SR.FormatID0334("data"))), + // Linear returns a nested "viewer" object that is itself nested in a GraphQL "data" node. + ProviderTypes.Linear => new(context.Response["data"]?["viewer"]?.GetNamedParameters() ?? + throw new InvalidOperationException(SR.FormatID0334("data/viewer"))), + // Meetup returns a nested "self" object that is itself nested in a GraphQL "data" node. ProviderTypes.Meetup => new(context.Response["data"]?["self"]?.GetNamedParameters() ?? throw new InvalidOperationException(SR.FormatID0334("data/self"))), + // Miro returns a nested "user" object, as well as a nested "team" and "organization". + ProviderTypes.Miro => new(context.Response["user"]?.GetNamedParameters() ?? + throw new InvalidOperationException(SR.FormatID0334("user"))) + { + ["organization"] = context.Response["organization"], + ["team"] = context.Response["team"] + }, + // Nextcloud returns a nested "data" object that is itself nested in a "ocs" node. ProviderTypes.Nextcloud => new(context.Response["ocs"]?["data"]?.GetNamedParameters() ?? throw new InvalidOperationException(SR.FormatID0334("ocs/data"))), diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index c9d73c50..ef10274a 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -1147,6 +1147,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.UserInfoRequest["fields"] = string.Join(",", settings.Fields); } + // Linear's userinfo endpoint is a GraphQL implementation that requires + // sending a proper "query" parameter containing the requested user details. + else if (context.Registration.ProviderType is ProviderTypes.Linear) + { + var settings = context.Registration.GetLinearSettings(); + + context.UserInfoRequest["query"] = $"query {{ viewer {{ {string.Join(" ", settings.UserFields)} }} }}"; + } + // Meetup's userinfo endpoint is a GraphQL implementation that requires // sending a proper "query" parameter containing the requested user details. else if (context.Registration.ProviderType is ProviderTypes.Meetup) @@ -1503,15 +1512,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers ProviderTypes.Atlassian => (string?) context.UserInfoResponse?["account_id"], // These providers return the user identifier as a custom "id" node: - ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or - ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or - ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.Gitee or - ProviderTypes.GitHub or ProviderTypes.Harvest or ProviderTypes.Kook or - ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Mastodon or - ProviderTypes.Meetup or ProviderTypes.Nextcloud or ProviderTypes.Patreon or - ProviderTypes.Pipedrive or ProviderTypes.Reddit or ProviderTypes.Smartsheet or - ProviderTypes.Spotify or ProviderTypes.SubscribeStar or ProviderTypes.Todoist or - ProviderTypes.Twitter or ProviderTypes.Weibo or ProviderTypes.Yandex or + ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or + ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or + ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.Gitee or + ProviderTypes.GitHub or ProviderTypes.Harvest or ProviderTypes.Kook or + ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Linear or + ProviderTypes.Mastodon or ProviderTypes.Meetup or ProviderTypes.Miro or + ProviderTypes.Nextcloud or ProviderTypes.Patreon or ProviderTypes.Pipedrive or + ProviderTypes.Reddit or ProviderTypes.Smartsheet or ProviderTypes.Spotify or + ProviderTypes.SubscribeStar or ProviderTypes.Todoist or ProviderTypes.Twitter or + ProviderTypes.Webflow or ProviderTypes.Weibo or ProviderTypes.Yandex or ProviderTypes.Zoom => (string?) context.UserInfoResponse?["id"], @@ -1917,6 +1927,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Request.Display = settings.Display; } + // Linear allows setting the prompt parameter (setting it to "consent" will + // force the consent screen to be displayed for each authorization request). + else if (context.Registration.ProviderType is ProviderTypes.Linear) + { + var settings = context.Registration.GetLinearSettings(); + + context.Request.Prompt = settings.Prompt; + } + // By default, MusicBrainz doesn't return a refresh token but allows sending an "access_type" // parameter to retrieve one (but it is only returned during the first authorization dance). else if (context.Registration.ProviderType is ProviderTypes.MusicBrainz) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 95b1eb99..84feecd9 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -1069,6 +1069,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +