diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index 927f27b16..f091bc0ec 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D}" EndProject @@ -101,6 +101,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Patre EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.QQ", "src\AspNet.Security.OAuth.QQ\AspNet.Security.OAuth.QQ.csproj", "{7C2E82CE-F6EC-41A8-AA22-3466505F95D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Shopify", "src\AspNet.Security.OAuth.Shopify\AspNet.Security.OAuth.Shopify.csproj", "{4D360329-84C7-4342-9462-46F742485E0F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -295,6 +297,10 @@ Global {7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C2E82CE-F6EC-41A8-AA22-3466505F95D8}.Release|Any CPU.Build.0 = Release|Any CPU + {4D360329-84C7-4342-9462-46F742485E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D360329-84C7-4342-9462-46F742485E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D360329-84C7-4342-9462-46F742485E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D360329-84C7-4342-9462-46F742485E0F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -347,5 +353,9 @@ Global {86614CB9-0768-40BF-8C27-699E3990B733} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {ED8A220C-45FE-45C6-9B8F-BB009ACE972E} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {7C2E82CE-F6EC-41A8-AA22-3466505F95D8} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {4D360329-84C7-4342-9462-46F742485E0F} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {01FFE8B9-2867-4ECE-9660-8745BABB5540} EndGlobalSection EndGlobal diff --git a/samples/Mvc.Client/Controllers/AuthenticationController.cs b/samples/Mvc.Client/Controllers/AuthenticationController.cs index 26feec2a9..d83440356 100644 --- a/samples/Mvc.Client/Controllers/AuthenticationController.cs +++ b/samples/Mvc.Client/Controllers/AuthenticationController.cs @@ -5,6 +5,7 @@ */ using System.Threading.Tasks; +using AspNet.Security.OAuth.Shopify; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; @@ -15,7 +16,11 @@ namespace Mvc.Client.Controllers public class AuthenticationController : Controller { [HttpGet("~/signin")] - public async Task SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync()); + public async Task SignIn() + { + var providers = await HttpContext.GetExternalProvidersAsync(); + return View("SignIn", providers); + } [HttpPost("~/signin")] public async Task SignIn([FromForm] string provider) @@ -35,7 +40,31 @@ public async Task SignIn([FromForm] string provider) // Instruct the middleware corresponding to the requested external identity // provider to redirect the user agent to its own authorization endpoint. // Note: the authenticationScheme parameter must match the value configured in Startup.cs - return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider); + if (provider != ShopifyAuthenticationDefaults.AuthenticationScheme) + { + return Challenge(new AuthenticationProperties + { + RedirectUri = "/", + }, provider); + } + + + // Shopify OAuth differs from most (all?) others in that you need to know the host name of the + // shop in order to construct the authorization endpoint. This can be aquired either from the + // user directly, or provided by the shopify app store during app install/activation. + var authProps = new ShopifyAuthenticationProperties("ctest-205") // Put your shop name here. + { + RedirectUri = "/", + + // Override OAuthOptions.Scope. Must be fully formatted. + //Scope = "read_customers,read_orders" + + // Set to true for a per-user, online-only, token. The retured token has an expiration date + // and should not be persisted. An offline token is requested by default. + //RequestPerUserToken = true + }; + + return Challenge(authProps, provider); } [HttpGet("~/signout"), HttpPost("~/signout")] diff --git a/samples/Mvc.Client/Mvc.Client.csproj b/samples/Mvc.Client/Mvc.Client.csproj index 1c6d93c2f..24676073e 100644 --- a/samples/Mvc.Client/Mvc.Client.csproj +++ b/samples/Mvc.Client/Mvc.Client.csproj @@ -36,6 +36,7 @@ + diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 8c09d8f8a..2cf056aac 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -4,6 +4,7 @@ * for more information concerning the license and the contributors participating to this project. */ +using System.Collections.Generic; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -42,6 +43,15 @@ public void ConfigureServices(IServiceCollection services) options.ClientId = "49e302895d8b09ea5656"; options.ClientSecret = "98f1bf028608901e9df91d64ee61536fe562064b"; options.Scope.Add("user:email"); + }) + + .AddShopify(options => + { + options.ApiKey = "9489b7555256142d9d723711c4614df7"; + options.ApiSecretKey = "2aa67425c36ad047f579ee594a59269d"; + + options.Scope.Add("read_customers"); + options.Scope.Add("read_orders"); }); services.AddMvc(); diff --git a/src/AspNet.Security.OAuth.Shopify/AspNet.Security.OAuth.Shopify.csproj b/src/AspNet.Security.OAuth.Shopify/AspNet.Security.OAuth.Shopify.csproj new file mode 100644 index 000000000..d9b364565 --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/AspNet.Security.OAuth.Shopify.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + Chris Boaro + aspnet-contrib + Chris Boaro + ASP.NET Core security middleware enabling Shopify authentication. + http://www.apache.org/licenses/LICENSE-2.0.html + https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + https://avatars3.githubusercontent.com/u/7998081?s=64 + git://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + git + aspnetcore;authentication;shopify;oauth;security + 2.0.0.0 + 2.0.0.0 + false + key.snk + 1.0.1 + + + + bin\Debug\netstandard2.0\ + bin\Debugobj\Debug\netstandard2.0\AspNet.Security.OAuth.Shopify.xml + + + + + + + + diff --git a/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationDefaults.cs new file mode 100644 index 000000000..348a0b5b0 --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationDefaults.cs @@ -0,0 +1,97 @@ +/* + * 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 Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace AspNet.Security.OAuth.Shopify +{ + /// + /// Default values used by the Shopify authentication middleware. + /// + public static class ShopifyAuthenticationDefaults + { + /// + /// Default value for . + /// + public const string AuthenticationScheme = "Shopify"; + + /// + /// Default value for . + /// + public const string DisplayName = "Shopify"; + + /// + /// Default value for . + /// + public const string Issuer = "Shopify"; + + /// + /// Default value for . + /// + public const string CallbackPath = "/signin-shopify"; + + /// + /// Default value for . + /// + public const string AuthorizationEndpoint = "https://{0}.myshopify.com/admin/oauth/authorize"; + + /// + /// Default value for . + /// + public const string TokenEndpoint = "https://{0}.myshopify.com/admin/oauth/access_token"; + + /// + /// Default value for . + /// + public const string UserInformationEndpoint = "https://{0}.myshopify.com/admin/shop"; + + + /// + /// Name of dictionary entry in that contains + /// the name of the shop. + /// + public const string ShopNameAuthenticationProperty = "ShopName"; + + /// + /// Set this authentication property to override the scope set in . Note - if this + /// override is used, it must be fully formatted. + /// + public const string ShopScopeAuthenticationProperty = "Scope"; + + /// + /// Additional grant options. The only acceptable value is "per-user" + /// + public const string GrantOptionsAuthenticationProperty = "GrantOptions"; + + /// + /// Per user is the only acceptable grant option at this time. + /// + public const string PerUserAuthenticationPropertyValue = "per-user"; + + + /// + /// The claim type which contains the permission scope returned by Shopify during authorization. + /// This may not be the same scope requested, so apps should verify they have the scope they need. + /// + public const string ShopifyScopeClaimType = "urn:shopify:scope"; + + /// + /// The plan name that this shop is using. + /// + public const string ShopifyPlanNameClaimType = "urn:shopify:plan_name"; + + /// + /// Claim type indicating whether or not this shop if eligable to make payments. + /// + public const string ShopifyEligableForPaymentsClaimType = "urn:shopify:eligible_for_payments"; + + /// + /// The timezone that that the shop is using. + /// + public const string ShopifyTimezoneClaimType = "urn:shopify:timezone"; + } +} diff --git a/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationExtensions.cs new file mode 100644 index 000000000..c460748c2 --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationExtensions.cs @@ -0,0 +1,79 @@ +/* + * 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; +using AspNet.Security.OAuth.Shopify; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods to add Shopify authentication capabilities to an HTTP application pipeline. + /// + public static class ShopifyAuthenticationExtensions + { + /// + /// Adds to the specified + /// , which enables Shopify authentication capabilities. + /// + /// The authentication builder. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddShopify([NotNull] this AuthenticationBuilder builder) + { + return builder.AddShopify(ShopifyAuthenticationDefaults.AuthenticationScheme, options => { }); + } + + /// + /// Adds to the specified + /// , which enables Shopify authentication capabilities. + /// + /// The authentication builder. + /// The delegate used to configure the OpenID 2.0 options. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddShopify( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddShopify(ShopifyAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables Shopify authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the Shopify options. + /// The . + public static AuthenticationBuilder AddShopify( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddShopify(scheme, ShopifyAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables Shopify 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 Shopify options. + /// The . + public static AuthenticationBuilder AddShopify( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [CanBeNull] string caption, + [NotNull] Action configuration) + { + return builder.AddOAuth(scheme, caption, configuration); + } + + } +} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationHandler.cs new file mode 100644 index 000000000..0c60c651b --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationHandler.cs @@ -0,0 +1,218 @@ +/* + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AspNet.Security.OAuth.Shopify +{ + /// + [UsedImplicitly] + public class ShopifyAuthenticationHandler : OAuthHandler + { + /// + public ShopifyAuthenticationHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder, + [NotNull] ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + /// + protected override async Task CreateTicketAsync( + [NotNull] ClaimsIdentity identity, + [NotNull] AuthenticationProperties properties, + [NotNull] OAuthTokenResponse tokens) + { + var uri = string.Format(ShopifyAuthenticationDefaults.UserInformationEndpoint, + properties.Items[ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty]); + + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Add("X-Shopify-Access-Token", tokens.AccessToken); + + var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + Logger.LogError("An error occurred while retrieving the user profile: the remote server " + + "returned a {Status} response with the following payload: {Headers} {Body}.", + /* Status: */ response.StatusCode, + /* Headers: */ response.Headers.ToString(), + /* Body: */ await response.Content.ReadAsStringAsync()); + + throw new HttpRequestException("An error occurred while retrieving the shop profile."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + +#if DEBUG + // ReSharper disable once UnusedVariable + var jsonStr = payload.ToString(Formatting.Indented); +#endif + + // In Shopify, the customer can modify the scope given to the app. Apps should verify + // that the customer is allowing the required scope. + var actualScope = tokens.Response["scope"].ToString(); + var isPersistent = true; + + // If the request was for a "per-user" (i.e. no offline access) + if (tokens.Response.TryGetValue("expires_in", out var val)) + { + isPersistent = false; + + var expires = DateTimeOffset.Now.AddSeconds(Convert.ToInt32(val.ToString())); + identity.AddClaim(new Claim(ClaimTypes.Expiration, expires.ToString("O"), ShopifyAuthenticationOptions.XmlSchemaDateTime)); + + actualScope = tokens.Response["associated_user_scope"].ToString(); + + var userData = tokens.Response["associated_user"].ToString(); + identity.AddClaim(new Claim(ClaimTypes.UserData, userData)); + } + + identity.AddClaim(new Claim(ClaimTypes.IsPersistent, isPersistent.ToString(), ShopifyAuthenticationOptions.XmlSchemaBoolean)); + identity.AddClaim(new Claim(ShopifyAuthenticationDefaults.ShopifyScopeClaimType, actualScope)); + + var principal = new ClaimsPrincipal(identity); + + var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload); + context.RunClaimActions(payload); + + await Options.Events.CreatingTicket(context); + var ticket = new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); + + return ticket; + } + + /// + protected override string FormatScope() + { + return string.Join(",", Options.Scope); + } + + /// + protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) + { + if (!properties.Items.ContainsKey(ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty)) + { + var msg = + $"Shopify provider AuthenticationProperties must contain {ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty}"; + + Logger.LogError(msg); + throw new Exception(msg); + } + + var shopName = properties.Items[ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty]; + var uri = string.Format(Options.AuthorizationEndpoint, shopName); + + // Get the permission scope, which can either be set in options or overridden in AuthenticationProperties. + var scope = properties.Items.ContainsKey(ShopifyAuthenticationDefaults.ShopScopeAuthenticationProperty) ? + properties.Items[ShopifyAuthenticationDefaults.ShopScopeAuthenticationProperty] : + FormatScope(); + + var url = QueryHelpers.AddQueryString(uri, new Dictionary + { + ["client_id"] = Options.ClientId, + ["scope"] = scope, + ["redirect_uri"] = redirectUri, + ["state"] = Options.StateDataFormat.Protect(properties) + }); + + // If we're requesting a per-user, online only, token, add the grant_options query param. + if (properties.Items.ContainsKey(ShopifyAuthenticationDefaults.GrantOptionsAuthenticationProperty)) + { + var grantOptions = properties.Items[ShopifyAuthenticationDefaults.GrantOptionsAuthenticationProperty]; + if (grantOptions != null && + grantOptions.Equals(ShopifyAuthenticationDefaults.PerUserAuthenticationPropertyValue)) + { + url = QueryHelpers.AddQueryString(url, "grant_options[]", + ShopifyAuthenticationDefaults.PerUserAuthenticationPropertyValue); + } + } + + return url; + } + + /// + protected override async Task ExchangeCodeAsync( + [NotNull] string code, + [NotNull] string redirectUri) + { + string shopDns; + + try + { + var shop = Context.Request.Query["shop"]; + var state = Context.Request.Query["state"]; + + // Shop name must end with myshopify.com + if (!shop.ToString().EndsWith(".myshopify.com")) + { + throw new Exception("Unexpected query string."); + } + + // Strip out the "myshopify.com" suffix + shopDns = shop.ToString().Split('.').First(); + + // Verify that the shop name encoded in "state" matches the shop name we used to + // request the token. This probably isn't necessary, but it's an easy extra verification. + var z = Options.StateDataFormat.Unprotect(state); + if (!z.Items[ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty] + .Equals(shopDns, StringComparison.InvariantCultureIgnoreCase)) + { + throw new Exception("Unexpected query string"); + } + } + catch (Exception e) + { + Logger.LogError("An error occurred while exchanging tokens: " + e.Message); + return OAuthTokenResponse.Failed(e); + } + + var uri = string.Format(Options.TokenEndpoint, shopDns); + var request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded")); + + request.Content = new FormUrlEncodedContent(new Dictionary + { + ["client_id"] = Options.ClientId, + ["client_secret"] = Options.ClientSecret, + ["code"] = code + }); + + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + + if (!response.IsSuccessStatusCode) + { + Logger.LogError("An error occurred while retrieving an access token: the remote server " + + "returned a {Status} response with the following payload: {Headers} {Body}.", + /* Status: */ response.StatusCode, + /* Headers: */ response.Headers.ToString(), + /* Body: */ await response.Content.ReadAsStringAsync()); + + return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + return OAuthTokenResponse.Success(payload); + } + } +} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationOptions.cs new file mode 100644 index 000000000..b6d1170d6 --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationOptions.cs @@ -0,0 +1,69 @@ +/* + * 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 Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Http; + +namespace AspNet.Security.OAuth.Shopify +{ + /// + public class ShopifyAuthenticationOptions : OAuthOptions + { + /// + /// Claim valueType used for boolean claims. + /// + public const string XmlSchemaBoolean = "http://www.w3.org/2001/XMLSchema#boolean"; + + /// + /// Claim valueType used for dateTime claims (iso format) + /// + public const string XmlSchemaDateTime = "http://www.w3.org/2001/XMLSchema#dateTime"; + + /// + /// An alias for + /// + public string ApiKey + { + get => ClientId; + set => ClientId = value; + } + + /// + /// An alias for + /// + public string ApiSecretKey + { + get => ClientSecret; + set => ClientSecret = value; + } + + /// + /// Constructor + /// + public ShopifyAuthenticationOptions() + { + ClaimsIssuer = ShopifyAuthenticationDefaults.Issuer; + CallbackPath = new PathString(ShopifyAuthenticationDefaults.CallbackPath); + AuthorizationEndpoint = ShopifyAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = ShopifyAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = ShopifyAuthenticationDefaults.UserInformationEndpoint; + + ClaimActions.MapJsonSubKey(ClaimTypes.NameIdentifier, "shop", "myshopify_domain"); + ClaimActions.MapJsonSubKey(ClaimTypes.Name, "shop", "name"); + ClaimActions.MapJsonSubKey(ClaimTypes.Webpage, "shop", "domain"); + ClaimActions.MapJsonSubKey(ClaimTypes.Email, "shop", "email"); + ClaimActions.MapJsonSubKey(ClaimTypes.Country, "shop", "country_code"); + ClaimActions.MapJsonSubKey(ClaimTypes.StateOrProvince, "shop", "province_code"); + ClaimActions.MapJsonSubKey(ClaimTypes.PostalCode, "shop", "zip"); + ClaimActions.MapJsonSubKey(ClaimTypes.Locality, "shop", "primary_locale"); + ClaimActions.MapJsonSubKey(ShopifyAuthenticationDefaults.ShopifyPlanNameClaimType, "shop", "plan_name"); + ClaimActions.MapJsonSubKey(ShopifyAuthenticationDefaults.ShopifyEligableForPaymentsClaimType, "shop", "eligible_for_payments", XmlSchemaBoolean); + ClaimActions.MapJsonSubKey(ShopifyAuthenticationDefaults.ShopifyTimezoneClaimType, "shop", "timezone"); + } + } +} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationProperties.cs b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationProperties.cs new file mode 100644 index 000000000..784414a73 --- /dev/null +++ b/src/AspNet.Security.OAuth.Shopify/ShopifyAuthenticationProperties.cs @@ -0,0 +1,95 @@ +/* + * 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.Collections.Generic; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace AspNet.Security.OAuth.Shopify +{ + /// + /// + /// Substitue for to enforce setting shop name + /// before Challenge and provide an override for . You + /// can accomplish the same thing by setting the approriate values in . + /// + public class ShopifyAuthenticationProperties : AuthenticationProperties + { + /// + /// Override . Must be fully formatted. + /// + public string Scope + { + get => GetProperty(ShopifyAuthenticationDefaults.ShopScopeAuthenticationProperty); + set => SetProperty(ShopifyAuthenticationDefaults.ShopScopeAuthenticationProperty, value); + } + + /// + /// Request a per user token. No offline access, do not persist. + /// + public bool RequestPerUserToken + { + get + { + var prop = GetProperty(ShopifyAuthenticationDefaults.GrantOptionsAuthenticationProperty); + return prop != null && prop.Equals(ShopifyAuthenticationDefaults.PerUserAuthenticationPropertyValue); + } + + set => SetProperty(ShopifyAuthenticationDefaults.GrantOptionsAuthenticationProperty, + value ? + ShopifyAuthenticationDefaults.PerUserAuthenticationPropertyValue : + null); + } + + /// + /// The name of the shop. Unlike most OAuth providers, the Shop name needs to be known in order + /// to authorize. This must either be gotten from the user or sent from Shopify during App store + /// installation. + /// + private string ShopName + { + set => SetProperty(ShopifyAuthenticationDefaults.ShopNameAuthenticationProperty, value); + } + + private void SetProperty(string propName, string value) + { + if (Items.ContainsKey(propName)) + Items[propName] = value; + else + Items.Add(propName, value); + } + + private string GetProperty(string propName) + { + return Items.TryGetValue(propName, out var val) ? val : null; + } + + /// + /// + /// + /// The name of the shop. Unlike most OAuth providers, the Shop name needs to be known in order + /// to authorize. This must either be gotten from the user or sent from Shopify during App store + /// installation. + /// + public ShopifyAuthenticationProperties(string shopName) + { + ShopName = shopName; + } + + /// + /// + /// + /// The name of the shop. Unlike most OAuth providers, the Shop name needs to be known in order + /// to authorize. This must either be gotten from the user or sent from Shopify during App store + /// installation. + /// + /// Set Items values. + public ShopifyAuthenticationProperties(string shopName, IDictionary items) : base(items) + { + ShopName = shopName; + } + } +} \ No newline at end of file