diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index b9ec15bed..6c017accf 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -221,6 +221,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A docs\workweixin.md = docs\workweixin.md docs\xumm.md = docs\xumm.md docs\zendesk.md = docs\zendesk.md + docs\linear.md = docs\linear.md EndProjectSection EndProject 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 EndProject 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}" EndProject +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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -740,6 +743,10 @@ Global {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.Build.0 = Release|Any CPU + {B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1167108-CA36-4C6B-85B0-1C7F5A24E4A4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -855,6 +862,7 @@ Global {F3E62C24-5F82-4CF5-A994-0E10D04FB495} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {668833D5-DB6A-475F-B0FD-A03462B037B8} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {B1167108-CA36-4C6B-85B0-1C7F5A24E4A4} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C} diff --git a/README.md b/README.md index d0cf81be2..a51067286 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ If a provider you're looking for does not exist, consider making a PR to add one | 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") | | 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") | | 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") | +| 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") | | 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") | | 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") | | 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") | diff --git a/docs/README.md b/docs/README.md index 5a25cacdb..cef9dffa2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,7 @@ covered by the section above. | Instagram | _Optional_ | [Documentation](instagram.md "Instagram provider documentation") | | KOOK | _Optional_ | [Documentation](kook.md "KOOK provider documentation") | | Line | _Optional_ | [Documentation](line.md "Line provider documentation") | +| Linear | _Optional_ | [Documentation](linear.md "Linear provider documentation") | | LinkedIn | _Optional_ | [Documentation](linkedin.md "LinkedIn provider documentation") | | Odnoklassniki | _Optional_ | [Documentation](odnoklassniki.md "Odnoklassniki provider documentation") | | Okta | **Required** | [Documentation](okta.md "Okta provider documentation") | diff --git a/docs/linear.md b/docs/linear.md new file mode 100644 index 000000000..05c4483f0 --- /dev/null +++ b/docs/linear.md @@ -0,0 +1,26 @@ +# Integrating the Linear Provider + +## Example + +```csharp +services.AddAuthentication(options => /* Auth configuration */) + .AddLinear(options => + { + options.ClientId = configuration["Linear:ClientId"] ?? string.Empty; + options.ClientSecret = configuration["Linear:ClientSecret"] ?? string.Empty; + + // 'read' scope is added by default. Add additional scopes required + // options.Scope.Add("write"); + + // Additional authorization parameters can also be added + // options.AdditionalAuthorizationParameters.Add("prompt", "consent"); + }) +``` + +## Required Additional Settings + +_None._ + +## Optional Settings + +_None._ diff --git a/src/AspNet.Security.OAuth.Linear/AspNet.Security.OAuth.Linear.csproj b/src/AspNet.Security.OAuth.Linear/AspNet.Security.OAuth.Linear.csproj new file mode 100644 index 000000000..c567439e2 --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/AspNet.Security.OAuth.Linear.csproj @@ -0,0 +1,21 @@ + + + + 9.2.0 + + true + $(DefaultNetCoreTargetFramework) + + + + ASP.NET Core security middleware enabling Linear authentication. + Jerrie Pelser + aspnetcore;authentication;linear;oauth;security + + + + + + + + diff --git a/src/AspNet.Security.OAuth.Linear/LinearAuthenticationConstants.cs b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationConstants.cs new file mode 100644 index 000000000..d24bfc64a --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationConstants.cs @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Linear; + +/// +/// Contains constants specific to the . +/// +public static class LinearAuthenticationConstants +{ + public static class Claims + { + public const string OrganizationId = "urn:linear:organization_id"; + + public const string OrganizationName = "urn:linear:organization_name"; + + public const string OrganizationUrlKey = "urn:linear:organization_urlkey"; + } +} diff --git a/src/AspNet.Security.OAuth.Linear/LinearAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationDefaults.cs new file mode 100644 index 000000000..ffca4ec0c --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationDefaults.cs @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Linear; + +/// +/// Default values used by the Linear authentication middleware. +/// +public static class LinearAuthenticationDefaults +{ + /// + /// Default value for the . + /// + public const string AuthenticationScheme = "Linear"; + + /// + /// Default value for the . + /// + public static readonly string DisplayName = "Linear"; + + /// + /// Default value for the . + /// + public static readonly string Issuer = "Linear"; + + /// + /// Default value for the . + /// + public static readonly string CallbackPath = "/signin-linear"; + + /// + /// Default value for the . + /// + public static readonly string AuthorizationEndpoint = "https://linear.app/oauth/authorize"; + + /// + /// Default value for the . + /// + public static readonly string TokenEndpointFormat = "https://api.linear.app/oauth/token"; +} diff --git a/src/AspNet.Security.OAuth.Linear/LinearAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationExtensions.cs new file mode 100644 index 000000000..792af5dbc --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationExtensions.cs @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using AspNet.Security.OAuth.Linear; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class LinearAuthenticationExtensions +{ + /// + /// Adds to the specified + /// , which enables Linear authentication capabilities. + /// + /// The authentication builder. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddLinear([NotNull] this AuthenticationBuilder builder) + { + return builder.AddLinear(LinearAuthenticationDefaults.AuthenticationScheme, options => { }); + } + + /// + /// Adds to the specified + /// , which enables Linear authentication capabilities. + /// + /// The authentication builder. + /// The delegate used to configure the Linear options. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddLinear( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddLinear(LinearAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables Linear authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the Linear options. + /// The . + public static AuthenticationBuilder AddLinear( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddLinear(scheme, LinearAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables Linear authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The optional display name associated with this instance. + /// The delegate used to configure the Linear options. + /// The . + public static AuthenticationBuilder AddLinear( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] string caption, + [NotNull] Action configuration) + { + return builder.AddOAuth(scheme, caption, configuration); + } +} diff --git a/src/AspNet.Security.OAuth.Linear/LinearAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationHandler.cs new file mode 100644 index 000000000..946476633 --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationHandler.cs @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.Linear; + +public partial class LinearAuthenticationHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder) : OAuthHandler(options, logger, encoder) +{ + private const string UserQuery = """ + query { + viewer { + id + name + email + organization { + id + name + urlKey + } + } + } + """; + + protected override async Task CreateTicketAsync( + [NotNull] ClaimsIdentity identity, + [NotNull] AuthenticationProperties properties, + [NotNull] OAuthTokenResponse tokens) + { + using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.linear.app/graphql"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + request.Content = new StringContent( + JsonSerializer.Serialize(new GraphqlQuery(UserQuery), AppJsonSerializerContext.Default.GraphqlQuery), + Encoding.UTF8, + MediaTypeNames.Application.Json); + + using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + + if (!response.IsSuccessStatusCode) + { + await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted); + throw new HttpRequestException("An error occurred while retrieving the user profile from Linear."); + } + + using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); + + var principal = new ClaimsPrincipal(identity); + var context = new OAuthCreatingTicketContext(principal, + properties, + Context, + Scheme, + Options, + Backchannel, + tokens, + payload.RootElement.GetProperty("data").GetProperty("viewer")); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); + } + + [JsonSerializable(typeof(GraphqlQuery))] + private sealed partial class AppJsonSerializerContext : JsonSerializerContext; + + private sealed record GraphqlQuery([property: JsonPropertyName("query")] string Query); + + private static partial class Log + { + internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + UserProfileError( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + + [LoggerMessage(1, + LogLevel.Error, + "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + private static partial void UserProfileError( + ILogger logger, + System.Net.HttpStatusCode status, + string headers, + string body); + } +} diff --git a/src/AspNet.Security.OAuth.Linear/LinearAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationOptions.cs new file mode 100644 index 000000000..a2b7db78a --- /dev/null +++ b/src/AspNet.Security.OAuth.Linear/LinearAuthenticationOptions.cs @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Security.Claims; +using static AspNet.Security.OAuth.Linear.LinearAuthenticationConstants; + +namespace AspNet.Security.OAuth.Linear; + +/// +/// Defines a set of options used by . +/// +public class LinearAuthenticationOptions : OAuthOptions +{ + public LinearAuthenticationOptions() + { + ClaimsIssuer = LinearAuthenticationDefaults.Issuer; + CallbackPath = LinearAuthenticationDefaults.CallbackPath; + + AuthorizationEndpoint = LinearAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = LinearAuthenticationDefaults.TokenEndpointFormat; + + Scope.Add("read"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonSubKey(Claims.OrganizationId, "organization", "id"); + ClaimActions.MapJsonSubKey(Claims.OrganizationName, "organization", "name"); + ClaimActions.MapJsonSubKey(Claims.OrganizationUrlKey, "organization", "urlKey"); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Linear/LinearTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Linear/LinearTests.cs new file mode 100644 index 000000000..2f65c755c --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Linear/LinearTests.cs @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Linear; + +public class LinearTests(ITestOutputHelper outputHelper) : OAuthTests(outputHelper) +{ + public override string DefaultScheme => LinearAuthenticationDefaults.AuthenticationScheme; + + protected internal override void RegisterAuthentication(AuthenticationBuilder builder) + { + builder.AddLinear(options => + { + ConfigureDefaults(builder, options); + }); + } + + [Theory] + [InlineData(ClaimTypes.NameIdentifier, "the-user-id")] + [InlineData(ClaimTypes.Name, "Marty McFly")] + [InlineData(ClaimTypes.Email, "marty@hillvalley.com")] + [InlineData(LinearAuthenticationConstants.Claims.OrganizationId, "the-org-id")] + [InlineData(LinearAuthenticationConstants.Claims.OrganizationName, "Hill Valley Ventures")] + [InlineData(LinearAuthenticationConstants.Claims.OrganizationUrlKey, "hill-valley")] + public async Task Can_Sign_In_Using_Linear(string claimType, string claimValue) + { + await AuthenticateUserAndAssertClaimValue(claimType, claimValue); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Linear/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Linear/bundle.json new file mode 100644 index 000000000..e40613f33 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Linear/bundle.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "items": [ + { + "comment": "https://developers.linear.app/docs/oauth/authentication", + "uri": "https://api.linear.app/oauth/token", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "access_token": "00a21d8b0c4e2375114e49c067dfb81eb0d2076f48354714cd5df984d87b67cc", + "token_type": "Bearer", + "expires_in": 315705599, + "scope": "read" + } + }, + { + "comment": "https://developers.linear.app/docs/graphql/working-with-the-graphql-api", + "uri": "https://api.linear.app/graphql", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "data" : { + "viewer" : { + "id" : "the-user-id", + "name" : "Marty McFly", + "email" : "marty@hillvalley.com", + "organization" : { + "id" : "the-org-id", + "name" : "Hill Valley Ventures", + "urlKey" : "hill-valley" + } + } + } + } + } + ] +}