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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+