Skip to content

Commit 8ecee02

Browse files
authored
Add Linear, Miro and Webflow to the list of supported providers
1 parent 542b1ee commit 8ecee02

File tree

4 files changed

+202
-13
lines changed

4 files changed

+202
-13
lines changed

src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
*/
66

77
using System.Collections.Immutable;
8+
using System.Diagnostics;
89
using System.Net.Http;
910
using System.Net.Http.Headers;
11+
using System.Net.Http.Json;
12+
using OpenIddict.Client.SystemNetHttp;
1013
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
1114
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
1215
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
@@ -25,12 +28,64 @@ public static class Revocation
2528
MapNonStandardRequestParameters.Descriptor,
2629
OverrideHttpMethod.Descriptor,
2730
AttachBearerAccessToken.Descriptor,
31+
AttachNonStandardRequestPayload.Descriptor,
2832

2933
/*
3034
* Revocation response extraction:
3135
*/
3236
NormalizeContentType.Descriptor
3337
];
38+
39+
/// <summary>
40+
/// Contains the logic responsible for attaching a non-standard payload for the providers that require it.
41+
/// </summary>
42+
public sealed class AttachNonStandardRequestPayload : IOpenIddictClientHandler<PrepareRevocationRequestContext>
43+
{
44+
/// <summary>
45+
/// Gets the default descriptor definition assigned to this handler.
46+
/// </summary>
47+
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
48+
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
49+
.AddFilter<RequireHttpUri>()
50+
.UseSingletonHandler<AttachNonStandardRequestPayload>()
51+
.SetOrder(AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor.Order + 500)
52+
.SetType(OpenIddictClientHandlerType.BuiltIn)
53+
.Build();
54+
55+
/// <inheritdoc/>
56+
public ValueTask HandleAsync(PrepareRevocationRequestContext context)
57+
{
58+
if (context is null)
59+
{
60+
throw new ArgumentNullException(nameof(context));
61+
}
62+
63+
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
64+
65+
// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
66+
// this may indicate that the request was incorrectly processed by another client stack.
67+
var request = context.Transaction.GetHttpRequestMessage() ??
68+
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
69+
70+
request.Content = context.Registration.ProviderType switch
71+
{
72+
// The token revocation endpoints exposed by these providers
73+
// requires sending the request parameters as a JSON payload:
74+
ProviderTypes.Miro => JsonContent.Create(
75+
context.Transaction.Request,
76+
OpenIddictSerializer.Default.Request,
77+
new MediaTypeHeaderValue(OpenIddictClientSystemNetHttpConstants.MediaTypes.Json)
78+
{
79+
CharSet = OpenIddictClientSystemNetHttpConstants.Charsets.Utf8
80+
}),
81+
82+
_ => request.Content
83+
};
84+
85+
return default;
86+
}
87+
}
88+
3489

3590
/// <summary>
3691
/// Contains the logic responsible for mapping non-standard request parameters
@@ -56,13 +111,37 @@ public ValueTask HandleAsync(PrepareRevocationRequestContext context)
56111
throw new ArgumentNullException(nameof(context));
57112
}
58113

59-
// Weibo, VK ID and Yandex don't support the standard "token" parameter and
114+
// These providers don't support the standard "token" parameter and
60115
// require using the non-standard "access_token" parameter instead.
61-
if (context.Registration.ProviderType is ProviderTypes.Weibo or ProviderTypes.VkId or ProviderTypes.Yandex)
116+
if (context.Registration.ProviderType is
117+
ProviderTypes.VkId or ProviderTypes.Webflow or
118+
ProviderTypes.Weibo or ProviderTypes.Yandex)
119+
{
120+
context.Request.AccessToken = context.Token;
121+
context.Request.Token = null;
122+
context.Request.TokenTypeHint = null;
123+
}
124+
125+
// Linear requires only the access_token and no other parameters.
126+
else if (context.Registration.ProviderType is ProviderTypes.Linear)
62127
{
63128
context.Request.AccessToken = context.Token;
64129
context.Request.Token = null;
65130
context.Request.TokenTypeHint = null;
131+
context.Request.ClientId = null;
132+
}
133+
134+
// Miro uses a JSON payload that expects the non-standard
135+
// "accessToken", "clientId" and "clientSecret" properties.
136+
else if (context.Registration.ProviderType is ProviderTypes.Miro)
137+
{
138+
context.Request["accessToken"] = context.Token;
139+
context.Request["clientId"] = context.Request.ClientId;
140+
context.Request["clientSecret"] = context.Request.ClientSecret;
141+
context.Request.Token = null;
142+
context.Request.TokenTypeHint = null;
143+
context.Request.ClientId = null;
144+
context.Request.ClientSecret = null;
66145
}
67146

68147
return default;
@@ -148,6 +227,15 @@ public ValueTask HandleAsync(PrepareRevocationRequestContext context)
148227
context.Request.Token = null;
149228
}
150229

230+
// Miro requires using bearer authentication with the token that is going to be revoked.
231+
//
232+
// Note: the token property CANNOT be used here as the token parameter is mapped to "accessToken".
233+
else if (context.Registration.ProviderType is ProviderTypes.Miro &&
234+
(string?) context.Request["accessToken"] is { Length: > 0 } token)
235+
{
236+
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, token);
237+
}
238+
151239
return default;
152240
}
153241
}

src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public ValueTask HandleAsync(PrepareUserInfoRequestContext context)
7373
{
7474
// The userinfo endpoints exposed by these providers
7575
// are based on GraphQL, which requires using POST:
76-
ProviderTypes.Meetup or ProviderTypes.SubscribeStar => HttpMethod.Post,
76+
ProviderTypes.Linear or ProviderTypes.Meetup or ProviderTypes.SubscribeStar => HttpMethod.Post,
7777

7878
// The userinfo endpoints exposed by these providers
7979
// use custom protocols that require using POST:
@@ -282,7 +282,7 @@ public ValueTask HandleAsync(PrepareUserInfoRequestContext context)
282282
{
283283
// The userinfo endpoints exposed by these providers are based on GraphQL,
284284
// which requires sending the request parameters as a JSON payload:
285-
ProviderTypes.Meetup or ProviderTypes.SubscribeStar => JsonContent.Create(
285+
ProviderTypes.Linear or ProviderTypes.Meetup or ProviderTypes.SubscribeStar => JsonContent.Create(
286286
context.Transaction.Request,
287287
OpenIddictSerializer.Default.Request,
288288
new MediaTypeHeaderValue(MediaTypes.Json)
@@ -433,10 +433,22 @@ ProviderTypes.Patreon or ProviderTypes.Pipedrive or ProviderTypes.Twitter
433433
=> new(context.Response["data"]?.GetNamedParameters() ??
434434
throw new InvalidOperationException(SR.FormatID0334("data"))),
435435

436+
// Linear returns a nested "viewer" object that is itself nested in a GraphQL "data" node.
437+
ProviderTypes.Linear => new(context.Response["data"]?["viewer"]?.GetNamedParameters() ??
438+
throw new InvalidOperationException(SR.FormatID0334("data/viewer"))),
439+
436440
// Meetup returns a nested "self" object that is itself nested in a GraphQL "data" node.
437441
ProviderTypes.Meetup => new(context.Response["data"]?["self"]?.GetNamedParameters() ??
438442
throw new InvalidOperationException(SR.FormatID0334("data/self"))),
439443

444+
// Miro returns a nested "user" object, as well as a nested "team" and "organization".
445+
ProviderTypes.Miro => new(context.Response["user"]?.GetNamedParameters() ??
446+
throw new InvalidOperationException(SR.FormatID0334("user")))
447+
{
448+
["organization"] = context.Response["organization"],
449+
["team"] = context.Response["team"]
450+
},
451+
440452
// Nextcloud returns a nested "data" object that is itself nested in a "ocs" node.
441453
ProviderTypes.Nextcloud => new(context.Response["ocs"]?["data"]?.GetNamedParameters() ??
442454
throw new InvalidOperationException(SR.FormatID0334("ocs/data"))),

src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,15 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
11471147

11481148
context.UserInfoRequest["fields"] = string.Join(",", settings.Fields);
11491149
}
1150+
1151+
// Linear's userinfo endpoint is a GraphQL implementation that requires
1152+
// sending a proper "query" parameter containing the requested user details.
1153+
else if (context.Registration.ProviderType is ProviderTypes.Linear)
1154+
{
1155+
var settings = context.Registration.GetLinearSettings();
1156+
1157+
context.UserInfoRequest["query"] = $"query {{ viewer {{ {string.Join(" ", settings.UserFields)} }} }}";
1158+
}
11501159

11511160
// Meetup's userinfo endpoint is a GraphQL implementation that requires
11521161
// sending a proper "query" parameter containing the requested user details.
@@ -1504,15 +1513,16 @@ ProviderTypes.ArcGisOnline or ProviderTypes.Trakt
15041513
ProviderTypes.Atlassian => (string?) context.UserInfoResponse?["account_id"],
15051514

15061515
// These providers return the user identifier as a custom "id" node:
1507-
ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or
1508-
ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or
1509-
ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.Gitee or
1510-
ProviderTypes.GitHub or ProviderTypes.Harvest or ProviderTypes.Kook or
1511-
ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Mastodon or
1512-
ProviderTypes.Meetup or ProviderTypes.Nextcloud or ProviderTypes.Patreon or
1513-
ProviderTypes.Pipedrive or ProviderTypes.Reddit or ProviderTypes.Smartsheet or
1514-
ProviderTypes.Spotify or ProviderTypes.SubscribeStar or ProviderTypes.Todoist or
1515-
ProviderTypes.Twitter or ProviderTypes.Weibo or ProviderTypes.Yandex or
1516+
ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or
1517+
ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or
1518+
ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.Gitee or
1519+
ProviderTypes.GitHub or ProviderTypes.Harvest or ProviderTypes.Kook or
1520+
ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Linear or
1521+
ProviderTypes.Mastodon or ProviderTypes.Meetup or ProviderTypes.Miro or
1522+
ProviderTypes.Nextcloud or ProviderTypes.Patreon or ProviderTypes.Pipedrive or
1523+
ProviderTypes.Reddit or ProviderTypes.Smartsheet or ProviderTypes.Spotify or
1524+
ProviderTypes.SubscribeStar or ProviderTypes.Todoist or ProviderTypes.Twitter or
1525+
ProviderTypes.Webflow or ProviderTypes.Weibo or ProviderTypes.Yandex or
15161526
ProviderTypes.Zoom
15171527
=> (string?) context.UserInfoResponse?["id"],
15181528

@@ -1918,6 +1928,15 @@ public ValueTask HandleAsync(ProcessChallengeContext context)
19181928
context.Request.Display = settings.Display;
19191929
}
19201930

1931+
// Linear allows setting the prompt parameter (setting it to "consent" will
1932+
// force the consent screen to be displayed for each authorization request).
1933+
else if (context.Registration.ProviderType is ProviderTypes.Linear)
1934+
{
1935+
var settings = context.Registration.GetLinearSettings();
1936+
1937+
context.Request.Prompt = settings.Prompt;
1938+
}
1939+
19211940
// By default, MusicBrainz doesn't return a refresh token but allows sending an "access_type"
19221941
// parameter to retrieve one (but it is only returned during the first authorization dance).
19231942
else if (context.Registration.ProviderType is ProviderTypes.MusicBrainz)

src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,35 @@
10691069
</Environment>
10701070
</Provider>
10711071

1072+
<!--
1073+
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
1074+
██ ████▄ ▄██ ▀██ ██ ▄▄▄█ ▄▄▀██ ▄▄▀██
1075+
██ █████ ███ █ █ ██ ▄▄▄█ ▀▀ ██ ▀▀▄██
1076+
██ ▀▀ █▀ ▀██ ██▄ ██ ▀▀▀█ ██ ██ ██ ██
1077+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
1078+
-->
1079+
<Provider Name="Linear" Id="9d5f20c2-1b3d-4375-8eb0-c6fcef63c3f7"
1080+
Documentation="https://developers.linear.app/docs/oauth/authentication">
1081+
<Environment Issuer="https://linear.app/">
1082+
<Configuration AuthorizationEndpoint="https://linear.app/oauth/authorize"
1083+
RevocationEndpoint="https://api.linear.app/oauth/revoke"
1084+
TokenEndpoint="https://api.linear.app/oauth/token"
1085+
UserInfoEndpoint="https://api.linear.app/graphql">
1086+
<RevocationEndpointAuthMethod Value="none"/>
1087+
</Configuration>
1088+
</Environment>
1089+
1090+
<Setting PropertyName="UserFields" ParameterName="fields" Collection="true" Type="String"
1091+
Description="The list of user fields to expand from the GraphQL endpoint (by default, only basic fields are requested)">
1092+
<Item Value="email" Default="true" Required="false" />
1093+
<Item Value="id" Default="true" Required="false" />
1094+
<Item Value="name" Default="true" Required="false" />
1095+
</Setting>
1096+
1097+
<Setting PropertyName="Prompt" ParameterName="prompt" Type="String" Required="false"
1098+
Description="The value used as the 'prompt' parameter (can be set to 'consent' to display the consent form for each authorization demand)" />
1099+
</Provider>
1100+
10721101
<!--
10731102
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
10741103
██ ████▄ ▄██ ▀██ ██ █▀▄██ ▄▄▄██ ▄▄▀█▄ ▄██ ▀██ ██
@@ -1198,6 +1227,24 @@
11981227
Description="The tenant used to identify the Microsoft Entra instance (by default, the common tenant is used)" />
11991228
</Provider>
12001229

1230+
<!--
1231+
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
1232+
██ ▄▀▄ █▄ ▄██ ▄▄▀██ ▄▄▄ ██
1233+
██ █ █ ██ ███ ▀▀▄██ ███ ██
1234+
██ ███ █▀ ▀██ ██ ██ ▀▀▀ ██
1235+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
1236+
-->
1237+
1238+
<Provider Name="Miro" Id="4e9426b3-7fd5-480a-a89e-67a80cfc5622"
1239+
Documentation="https://developers.miro.com/docs/getting-started-with-oauth">
1240+
<Environment Issuer="https://miro.com/">
1241+
<Configuration AuthorizationEndpoint="https://miro.com/oauth/authorize"
1242+
RevocationEndpoint="https://api.miro.com/v2/oauth/revoke"
1243+
TokenEndpoint="https://api.miro.com/v1/oauth/token"
1244+
UserInfoEndpoint="https://api.miro.com/v1/oauth-token" />
1245+
</Environment>
1246+
</Provider>
1247+
12011248
<!--
12021249
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
12031250
██ ▄▀▄ █▄ ▄█▄▀█▀▄██ ▄▄▀██ █████ ▄▄▄ ██ ██ ██ ▄▄▀██
@@ -2137,6 +2184,29 @@
21372184
<Environment Issuer="https://www.webex.com/" ConfigurationEndpoint="https://webexapis.com/v1/.well-known/openid-configuration" />
21382185
</Provider>
21392186

2187+
<!--
2188+
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
2189+
██ ███ ██ ▄▄▄██ ▄▄▀██ ▄▄▄██ █████ ▄▄▄ ██ ███ ██
2190+
██ █ █ ██ ▄▄▄██ ▄▄▀██ ▄▄███ █████ ███ ██ █ █ ██
2191+
██▄▀▄▀▄██ ▀▀▀██ ▀▀ ██ █████ ▀▀ ██ ▀▀▀ ██▄▀▄▀▄██
2192+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
2193+
-->
2194+
2195+
<Provider Name="Webflow" Id="87ec6fc6-771c-46a9-a8e9-f0049927536e"
2196+
Documentation="https://developers.webflow.com/v2.0.0/data/reference/oauth-app">
2197+
<Environment Issuer="https://webflow.com/">
2198+
<Configuration AuthorizationEndpoint="https://webflow.com/oauth/authorize"
2199+
RevocationEndpoint="https://webflow.com/oauth/revoke_authorization"
2200+
TokenEndpoint="https://api.webflow.com/oauth/access_token"
2201+
UserInfoEndpoint="https://api.webflow.com/v2/token/authorized_by" />
2202+
<!--
2203+
Note: Webflow requires sending the "authorized_user:read" scope to be able to use the userinfo endpoint.
2204+
-->
2205+
2206+
<Scope Name="authorized_user:read" Default="true" Required="true" />
2207+
</Environment>
2208+
</Provider>
2209+
21402210
<!--
21412211
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
21422212
██ ███ ██ ▄▄▄█▄ ▄██ ▄▄▀██ ▄▄▄ ██

0 commit comments

Comments
 (0)