From 8ecee0289201f56d9fdf852793c79a369dd463b1 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 | 92 ++++++++++++++++++- ...ctClientWebIntegrationHandlers.Userinfo.cs | 16 +++- .../OpenIddictClientWebIntegrationHandlers.cs | 37 ++++++-- ...penIddictClientWebIntegrationProviders.xml | 70 ++++++++++++++ 4 files changed, 202 insertions(+), 13 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs index 7fdb2555..c0f1293b 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs @@ -5,8 +5,11 @@ */ 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.OpenIddictClientSystemNetHttpHandlerFilters; using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; @@ -25,12 +28,64 @@ 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, + OpenIddictSerializer.Default.Request, + new MediaTypeHeaderValue(OpenIddictClientSystemNetHttpConstants.MediaTypes.Json) + { + CharSet = OpenIddictClientSystemNetHttpConstants.Charsets.Utf8 + }), + + _ => request.Content + }; + + return default; + } + } + /// /// Contains the logic responsible for mapping non-standard request parameters @@ -56,13 +111,37 @@ 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; @@ -148,6 +227,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 b9b02649..49c6c83d 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -73,7 +73,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: @@ -282,7 +282,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 => JsonContent.Create( + ProviderTypes.Linear or ProviderTypes.Meetup or ProviderTypes.SubscribeStar => JsonContent.Create( context.Transaction.Request, OpenIddictSerializer.Default.Request, new MediaTypeHeaderValue(MediaTypes.Json) @@ -433,10 +433,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 df37a9bb..a2e28509 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. @@ -1504,15 +1513,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"], @@ -1918,6 +1928,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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +