Skip to content

Commit 9c3cf46

Browse files
authored
Add Linear provider (#1045)
Add Linear provider.
1 parent 2a3bfb8 commit 9c3cf46

12 files changed

+398
-0
lines changed

AspNet.Security.OAuth.Providers.sln

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A
221221
docs\workweixin.md = docs\workweixin.md
222222
docs\xumm.md = docs\xumm.md
223223
docs\zendesk.md = docs\zendesk.md
224+
docs\linear.md = docs\linear.md
224225
EndProjectSection
225226
EndProject
226227
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Basecamp", "src\AspNet.Security.OAuth.Basecamp\AspNet.Security.OAuth.Basecamp.csproj", "{42306484-B2BF-4B52-B950-E0CDFA58B02A}"
@@ -318,6 +319,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.GitCo
318319
EndProject
319320
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Atlassian", "src\AspNet.Security.OAuth.Atlassian\AspNet.Security.OAuth.Atlassian.csproj", "{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}"
320321
EndProject
322+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Linear", "src\AspNet.Security.OAuth.Linear\AspNet.Security.OAuth.Linear.csproj", "{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}"
323+
EndProject
321324
Global
322325
GlobalSection(SolutionConfigurationPlatforms) = preSolution
323326
Debug|Any CPU = Debug|Any CPU
@@ -740,6 +743,10 @@ Global
740743
{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Debug|Any CPU.Build.0 = Debug|Any CPU
741744
{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.ActiveCfg = Release|Any CPU
742745
{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.Build.0 = Release|Any CPU
746+
{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
747+
{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
748+
{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
749+
{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Release|Any CPU.Build.0 = Release|Any CPU
743750
EndGlobalSection
744751
GlobalSection(SolutionProperties) = preSolution
745752
HideSolutionNode = FALSE
@@ -855,6 +862,7 @@ Global
855862
{F3E62C24-5F82-4CF5-A994-0E10D04FB495} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
856863
{668833D5-DB6A-475F-B0FD-A03462B037B8} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
857864
{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
865+
{B1167108-CA36-4C6B-85B0-1C7F5A24E4A4} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}
858866
EndGlobalSection
859867
GlobalSection(ExtensibilityGlobals) = postSolution
860868
SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ If a provider you're looking for does not exist, consider making a PR to add one
201201
| Kroger | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Kroger?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Kroger/ "Download AspNet.Security.OAuth.Kroger from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Kroger?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Kroger "Download AspNet.Security.OAuth.Kroger from MyGet.org") | [Documentation](https://developer.kroger.com/reference/#section/Authentication "Kroger developer documentation") |
202202
| Lichess | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Lichess?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Lichess/ "Download AspNet.Security.OAuth.Lichess from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Lichess?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Lichess "Download AspNet.Security.OAuth.Lichess from MyGet.org") | [Documentation](https://lichess.org/api#section/Authentication "Lichess developer documentation") |
203203
| Line | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Line?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Line/ "Download AspNet.Security.OAuth.Line from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Line?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Line "Download AspNet.Security.OAuth.Line from MyGet.org") | [Documentation](https://developers.line.biz/en/docs/line-login/integrate-line-login "Line developer documentation") |
204+
| Linear | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Linear?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Linear/ "Download AspNet.Security.OAuth.Linear from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Linear?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Linear "Download AspNet.Security.OAuth.Linear from MyGet.org") | [Documentation](https://developers.linear.app/docs/oauth/authentication "Linear developer documentation") |
204205
| LinkedIn | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.LinkedIn?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.LinkedIn/ "Download AspNet.Security.OAuth.LinkedIn from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.LinkedIn?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.LinkedIn "Download AspNet.Security.OAuth.LinkedIn from MyGet.org") | [Documentation](https://docs.microsoft.com/en-us/linkedin/shared/authentication/authentication "LinkedIn developer documentation") |
205206
| MailChimp | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.MailChimp?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.MailChimp/ "Download AspNet.Security.OAuth.MailChimp from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.MailChimp?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.MailChimp "Download AspNet.Security.OAuth.MailChimp from MyGet.org") | [Documentation](https://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-oauth2/ "MailChimp developer documentation") |
206207
| MailRu | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.MailRu?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.MailRu/ "Download AspNet.Security.OAuth.MailRu from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.MailRu?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.MailRu "Download AspNet.Security.OAuth.MailRu from MyGet.org") | [Documentation](https://o2.mail.ru/docs#web "MailRu developer documentation") |

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ covered by the section above.
5959
| Instagram | _Optional_ | [Documentation](instagram.md "Instagram provider documentation") |
6060
| KOOK | _Optional_ | [Documentation](kook.md "KOOK provider documentation") |
6161
| Line | _Optional_ | [Documentation](line.md "Line provider documentation") |
62+
| Linear | _Optional_ | [Documentation](linear.md "Linear provider documentation") |
6263
| LinkedIn | _Optional_ | [Documentation](linkedin.md "LinkedIn provider documentation") |
6364
| Odnoklassniki | _Optional_ | [Documentation](odnoklassniki.md "Odnoklassniki provider documentation") |
6465
| Okta | **Required** | [Documentation](okta.md "Okta provider documentation") |

docs/linear.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Integrating the Linear Provider
2+
3+
## Example
4+
5+
```csharp
6+
services.AddAuthentication(options => /* Auth configuration */)
7+
.AddLinear(options =>
8+
{
9+
options.ClientId = configuration["Linear:ClientId"] ?? string.Empty;
10+
options.ClientSecret = configuration["Linear:ClientSecret"] ?? string.Empty;
11+
12+
// 'read' scope is added by default. Add additional scopes required
13+
// options.Scope.Add("write");
14+
15+
// Additional authorization parameters can also be added
16+
// options.AdditionalAuthorizationParameters.Add("prompt", "consent");
17+
})
18+
```
19+
20+
## Required Additional Settings
21+
22+
_None._
23+
24+
## Optional Settings
25+
26+
_None._
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<PackageValidationBaselineVersion>9.2.0</PackageValidationBaselineVersion>
5+
<!-- TODO Remove once published to NuGet.org -->
6+
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
7+
<TargetFrameworks>$(DefaultNetCoreTargetFramework)</TargetFrameworks>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<Description>ASP.NET Core security middleware enabling Linear authentication.</Description>
12+
<Authors>Jerrie Pelser</Authors>
13+
<PackageTags>aspnetcore;authentication;linear;oauth;security</PackageTags>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
18+
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
namespace AspNet.Security.OAuth.Linear;
8+
9+
/// <summary>
10+
/// Contains constants specific to the <see cref="LinearAuthenticationHandler"/>.
11+
/// </summary>
12+
public static class LinearAuthenticationConstants
13+
{
14+
public static class Claims
15+
{
16+
public const string OrganizationId = "urn:linear:organization_id";
17+
18+
public const string OrganizationName = "urn:linear:organization_name";
19+
20+
public const string OrganizationUrlKey = "urn:linear:organization_urlkey";
21+
}
22+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
namespace AspNet.Security.OAuth.Linear;
8+
9+
/// <summary>
10+
/// Default values used by the Linear authentication middleware.
11+
/// </summary>
12+
public static class LinearAuthenticationDefaults
13+
{
14+
/// <summary>
15+
/// Default value for the <see cref="AuthenticationScheme.Name"/>.
16+
/// </summary>
17+
public const string AuthenticationScheme = "Linear";
18+
19+
/// <summary>
20+
/// Default value for the <see cref="AuthenticationScheme.DisplayName"/>.
21+
/// </summary>
22+
public static readonly string DisplayName = "Linear";
23+
24+
/// <summary>
25+
/// Default value for the <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>.
26+
/// </summary>
27+
public static readonly string Issuer = "Linear";
28+
29+
/// <summary>
30+
/// Default value for the <see cref="RemoteAuthenticationOptions.CallbackPath"/>.
31+
/// </summary>
32+
public static readonly string CallbackPath = "/signin-linear";
33+
34+
/// <summary>
35+
/// Default value for the <see cref="OAuthOptions.AuthorizationEndpoint"/>.
36+
/// </summary>
37+
public static readonly string AuthorizationEndpoint = "https://linear.app/oauth/authorize";
38+
39+
/// <summary>
40+
/// Default value for the <see cref="OAuthOptions.TokenEndpoint"/>.
41+
/// </summary>
42+
public static readonly string TokenEndpointFormat = "https://api.linear.app/oauth/token";
43+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using AspNet.Security.OAuth.Linear;
8+
9+
namespace Microsoft.Extensions.DependencyInjection;
10+
11+
public static class LinearAuthenticationExtensions
12+
{
13+
/// <summary>
14+
/// Adds <see cref="LinearAuthenticationHandler"/> to the specified
15+
/// <see cref="AuthenticationBuilder"/>, which enables Linear authentication capabilities.
16+
/// </summary>
17+
/// <param name="builder">The authentication builder.</param>
18+
/// <returns>A reference to this instance after the operation has completed.</returns>
19+
public static AuthenticationBuilder AddLinear([NotNull] this AuthenticationBuilder builder)
20+
{
21+
return builder.AddLinear(LinearAuthenticationDefaults.AuthenticationScheme, options => { });
22+
}
23+
24+
/// <summary>
25+
/// Adds <see cref="LinearAuthenticationHandler"/> to the specified
26+
/// <see cref="AuthenticationBuilder"/>, which enables Linear authentication capabilities.
27+
/// </summary>
28+
/// <param name="builder">The authentication builder.</param>
29+
/// <param name="configuration">The delegate used to configure the Linear options.</param>
30+
/// <returns>A reference to this instance after the operation has completed.</returns>
31+
public static AuthenticationBuilder AddLinear(
32+
[NotNull] this AuthenticationBuilder builder,
33+
[NotNull] Action<LinearAuthenticationOptions> configuration)
34+
{
35+
return builder.AddLinear(LinearAuthenticationDefaults.AuthenticationScheme, configuration);
36+
}
37+
38+
/// <summary>
39+
/// Adds <see cref="LinearAuthenticationHandler"/> to the specified
40+
/// <see cref="AuthenticationBuilder"/>, which enables Linear authentication capabilities.
41+
/// </summary>
42+
/// <param name="builder">The authentication builder.</param>
43+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
44+
/// <param name="configuration">The delegate used to configure the Linear options.</param>
45+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
46+
public static AuthenticationBuilder AddLinear(
47+
[NotNull] this AuthenticationBuilder builder,
48+
[NotNull] string scheme,
49+
[NotNull] Action<LinearAuthenticationOptions> configuration)
50+
{
51+
return builder.AddLinear(scheme, LinearAuthenticationDefaults.DisplayName, configuration);
52+
}
53+
54+
/// <summary>
55+
/// Adds <see cref="LinearAuthenticationHandler"/> to the specified
56+
/// <see cref="AuthenticationBuilder"/>, which enables Linear authentication capabilities.
57+
/// </summary>
58+
/// <param name="builder">The authentication builder.</param>
59+
/// <param name="scheme">The authentication scheme associated with this instance.</param>
60+
/// <param name="caption">The optional display name associated with this instance.</param>
61+
/// <param name="configuration">The delegate used to configure the Linear options.</param>
62+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
63+
public static AuthenticationBuilder AddLinear(
64+
[NotNull] this AuthenticationBuilder builder,
65+
[NotNull] string scheme,
66+
[NotNull] string caption,
67+
[NotNull] Action<LinearAuthenticationOptions> configuration)
68+
{
69+
return builder.AddOAuth<LinearAuthenticationOptions, LinearAuthenticationHandler>(scheme, caption, configuration);
70+
}
71+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System.Net.Http.Headers;
8+
using System.Net.Mime;
9+
using System.Security.Claims;
10+
using System.Text;
11+
using System.Text.Encodings.Web;
12+
using System.Text.Json;
13+
using System.Text.Json.Serialization;
14+
using Microsoft.Extensions.Logging;
15+
using Microsoft.Extensions.Options;
16+
17+
namespace AspNet.Security.OAuth.Linear;
18+
19+
public partial class LinearAuthenticationHandler(
20+
[NotNull] IOptionsMonitor<LinearAuthenticationOptions> options,
21+
[NotNull] ILoggerFactory logger,
22+
[NotNull] UrlEncoder encoder) : OAuthHandler<LinearAuthenticationOptions>(options, logger, encoder)
23+
{
24+
private const string UserQuery = """
25+
query {
26+
viewer {
27+
id
28+
name
29+
email
30+
organization {
31+
id
32+
name
33+
urlKey
34+
}
35+
}
36+
}
37+
""";
38+
39+
protected override async Task<AuthenticationTicket> CreateTicketAsync(
40+
[NotNull] ClaimsIdentity identity,
41+
[NotNull] AuthenticationProperties properties,
42+
[NotNull] OAuthTokenResponse tokens)
43+
{
44+
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.linear.app/graphql");
45+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
46+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
47+
request.Content = new StringContent(
48+
JsonSerializer.Serialize(new GraphqlQuery(UserQuery), AppJsonSerializerContext.Default.GraphqlQuery),
49+
Encoding.UTF8,
50+
MediaTypeNames.Application.Json);
51+
52+
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
53+
54+
if (!response.IsSuccessStatusCode)
55+
{
56+
await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted);
57+
throw new HttpRequestException("An error occurred while retrieving the user profile from Linear.");
58+
}
59+
60+
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
61+
62+
var principal = new ClaimsPrincipal(identity);
63+
var context = new OAuthCreatingTicketContext(principal,
64+
properties,
65+
Context,
66+
Scheme,
67+
Options,
68+
Backchannel,
69+
tokens,
70+
payload.RootElement.GetProperty("data").GetProperty("viewer"));
71+
context.RunClaimActions();
72+
73+
await Events.CreatingTicket(context);
74+
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
75+
}
76+
77+
[JsonSerializable(typeof(GraphqlQuery))]
78+
private sealed partial class AppJsonSerializerContext : JsonSerializerContext;
79+
80+
private sealed record GraphqlQuery([property: JsonPropertyName("query")] string Query);
81+
82+
private static partial class Log
83+
{
84+
internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken)
85+
{
86+
UserProfileError(
87+
logger,
88+
response.StatusCode,
89+
response.Headers.ToString(),
90+
await response.Content.ReadAsStringAsync(cancellationToken));
91+
}
92+
93+
[LoggerMessage(1,
94+
LogLevel.Error,
95+
"An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")]
96+
private static partial void UserProfileError(
97+
ILogger logger,
98+
System.Net.HttpStatusCode status,
99+
string headers,
100+
string body);
101+
}
102+
}

0 commit comments

Comments
 (0)