Browse Source

Add Linear, Miro and Webflow to the list of supported providers

pull/2288/head
Jerrie Pelser 10 months ago
committed by GitHub
parent
commit
8ecee02892
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 92
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs
  2. 16
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  3. 37
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  4. 70
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

92
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
];
/// <summary>
/// Contains the logic responsible for attaching a non-standard payload for the providers that require it.
/// </summary>
public sealed class AttachNonStandardRequestPayload : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.AddFilter<RequireHttpUri>()
.UseSingletonHandler<AttachNonStandardRequestPayload>()
.SetOrder(AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// 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;
}
}

16
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"))),

37
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)

70
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

@ -1069,6 +1069,35 @@
</Environment>
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ████▄ ▄██ ▀██ ██ ▄▄▄█ ▄▄▀██ ▄▄▀██
██ █████ ███ █ █ ██ ▄▄▄█ ▀▀ ██ ▀▀▄██
██ ▀▀ █▀ ▀██ ██▄ ██ ▀▀▀█ ██ ██ ██ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Linear" Id="9d5f20c2-1b3d-4375-8eb0-c6fcef63c3f7"
Documentation="https://developers.linear.app/docs/oauth/authentication">
<Environment Issuer="https://linear.app/">
<Configuration AuthorizationEndpoint="https://linear.app/oauth/authorize"
RevocationEndpoint="https://api.linear.app/oauth/revoke"
TokenEndpoint="https://api.linear.app/oauth/token"
UserInfoEndpoint="https://api.linear.app/graphql">
<RevocationEndpointAuthMethod Value="none"/>
</Configuration>
</Environment>
<Setting PropertyName="UserFields" ParameterName="fields" Collection="true" Type="String"
Description="The list of user fields to expand from the GraphQL endpoint (by default, only basic fields are requested)">
<Item Value="email" Default="true" Required="false" />
<Item Value="id" Default="true" Required="false" />
<Item Value="name" Default="true" Required="false" />
</Setting>
<Setting PropertyName="Prompt" ParameterName="prompt" Type="String" Required="false"
Description="The value used as the 'prompt' parameter (can be set to 'consent' to display the consent form for each authorization demand)" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ████▄ ▄██ ▀██ ██ █▀▄██ ▄▄▄██ ▄▄▀█▄ ▄██ ▀██ ██
@ -1198,6 +1227,24 @@
Description="The tenant used to identify the Microsoft Entra instance (by default, the common tenant is used)" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ▄▀▄ █▄ ▄██ ▄▄▀██ ▄▄▄ ██
██ █ █ ██ ███ ▀▀▄██ ███ ██
██ ███ █▀ ▀██ ██ ██ ▀▀▀ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Miro" Id="4e9426b3-7fd5-480a-a89e-67a80cfc5622"
Documentation="https://developers.miro.com/docs/getting-started-with-oauth">
<Environment Issuer="https://miro.com/">
<Configuration AuthorizationEndpoint="https://miro.com/oauth/authorize"
RevocationEndpoint="https://api.miro.com/v2/oauth/revoke"
TokenEndpoint="https://api.miro.com/v1/oauth/token"
UserInfoEndpoint="https://api.miro.com/v1/oauth-token" />
</Environment>
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ▄▀▄ █▄ ▄█▄▀█▀▄██ ▄▄▀██ █████ ▄▄▄ ██ ██ ██ ▄▄▀██
@ -2137,6 +2184,29 @@
<Environment Issuer="https://www.webex.com/" ConfigurationEndpoint="https://webexapis.com/v1/.well-known/openid-configuration" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ ██ ▄▄▄██ ▄▄▀██ ▄▄▄██ █████ ▄▄▄ ██ ███ ██
██ █ █ ██ ▄▄▄██ ▄▄▀██ ▄▄███ █████ ███ ██ █ █ ██
██▄▀▄▀▄██ ▀▀▀██ ▀▀ ██ █████ ▀▀ ██ ▀▀▀ ██▄▀▄▀▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Webflow" Id="87ec6fc6-771c-46a9-a8e9-f0049927536e"
Documentation="https://developers.webflow.com/v2.0.0/data/reference/oauth-app">
<Environment Issuer="https://webflow.com/">
<Configuration AuthorizationEndpoint="https://webflow.com/oauth/authorize"
RevocationEndpoint="https://webflow.com/oauth/revoke_authorization"
TokenEndpoint="https://api.webflow.com/oauth/access_token"
UserInfoEndpoint="https://api.webflow.com/v2/token/authorized_by" />
<!--
Note: Webflow requires sending the "authorized_user:read" scope to be able to use the userinfo endpoint.
-->
<Scope Name="authorized_user:read" Default="true" Required="true" />
</Environment>
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ ██ ▄▄▄█▄ ▄██ ▄▄▀██ ▄▄▄ ██

Loading…
Cancel
Save