diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index 477f2f0..f3f4ec6 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -265,6 +265,15 @@ + + + + + + + + + diff --git a/Owin.Security.Providers/Shopify/Constants.cs b/Owin.Security.Providers/Shopify/Constants.cs new file mode 100644 index 0000000..3a2a89a --- /dev/null +++ b/Owin.Security.Providers/Shopify/Constants.cs @@ -0,0 +1,7 @@ +namespace Owin.Security.Providers.Shopify +{ + internal static class Constants + { + public const string DefaultAuthenticationType = "Shopify"; + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/Provider/IShopifyAuthenticationProvider.cs b/Owin.Security.Providers/Shopify/Provider/IShopifyAuthenticationProvider.cs new file mode 100644 index 0000000..20df138 --- /dev/null +++ b/Owin.Security.Providers/Shopify/Provider/IShopifyAuthenticationProvider.cs @@ -0,0 +1,24 @@ +namespace Owin.Security.Providers.Shopify +{ + using System.Threading.Tasks; + + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IShopifyAuthenticationProvider + { + /// + /// Invoked whenever Shopify shop successfully authenticates a user. + /// + /// Contains information about the login session as well as the shop . + /// A representing the completed operation. + Task Authenticated(ShopifyAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// Instance of return endpoint context. + /// A representing the completed operation. + Task ReturnEndpoint(ShopifyReturnEndpointContext context); + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticatedContext.cs b/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticatedContext.cs new file mode 100644 index 0000000..c3ff120 --- /dev/null +++ b/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticatedContext.cs @@ -0,0 +1,78 @@ +namespace Owin.Security.Providers.Shopify +{ + using Microsoft.Owin; + using Microsoft.Owin.Security; + using Microsoft.Owin.Security.Provider; + using Newtonsoft.Json.Linq; + using System.Security.Claims; + + public class ShopifyAuthenticatedContext : BaseContext + { + /// + /// Initializes a new instance of the class. + /// + /// The OWIN environment. + /// The JSON-serialized shop. + /// Shopify shop access token. + public ShopifyAuthenticatedContext(IOwinContext context, JObject shop, string accessToken) + : base(context) + { + Shop = shop; + AccessToken = accessToken; + + Id = TryGetValue(shop, "id"); + var fullShopifyDomainName = TryGetValue(shop, "myshopify_domain"); + UserName = string.IsNullOrWhiteSpace(fullShopifyDomainName) ? null : fullShopifyDomainName.Replace(".myshopify.com", ""); + Email = TryGetValue(shop, "email"); + ShopName = TryGetValue(shop, "name"); + } + + /// + /// Gets the JSON-serialized Shopify shop. + /// + /// Contains the Shopify shop information obtained from the Shop endpoint. By default this is https://{shopname}.myshopify.com/admin/shop but it can be overridden in the options. + public JObject Shop { get; private set; } + + /// + /// Gets the Shopify shop access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Shopify shop Id. + /// + public string Id { get; private set; } + + /// + /// Gets the Shopify shop domain name. + /// + /// {shop_domain_name}.myshopify.com - without the ".myshopify.com" to be used as suggested username. + public string UserName { get; private set; } + + /// + /// Gets the Shopify shop primary email address. + /// + public string Email { get; private set; } + + /// + /// Gets the Shopify shop name. + /// + public string ShopName { get; private set; } + + /// + /// Gets the representing the Shopify shop. + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + + private static string TryGetValue(JToken shop, string propertyName) + { + var propertyValue = shop?.First?.First?[propertyName]; + return propertyValue?.ToString(); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticationProvider.cs b/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticationProvider.cs new file mode 100644 index 0000000..4e34eaa --- /dev/null +++ b/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticationProvider.cs @@ -0,0 +1,50 @@ +namespace Owin.Security.Providers.Shopify +{ + using System; + using System.Threading.Tasks; + + /// + /// Default implementation. + /// + public class ShopifyAuthenticationProvider : IShopifyAuthenticationProvider + { + /// + /// Initializes a new instance of the class. + /// + public ShopifyAuthenticationProvider() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Invoked whenever Shopify shop successfully authenticates a user. + /// + /// Contains information about the login session as well as the shop . + /// A representing the completed operation. + public virtual Task Authenticated(ShopifyAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// Instance of return endpoint context. + /// A representing the completed operation. + public virtual Task ReturnEndpoint(ShopifyReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/Provider/ShopifyReturnEndpointContext.cs b/Owin.Security.Providers/Shopify/Provider/ShopifyReturnEndpointContext.cs new file mode 100644 index 0000000..2305bfc --- /dev/null +++ b/Owin.Security.Providers/Shopify/Provider/ShopifyReturnEndpointContext.cs @@ -0,0 +1,22 @@ +namespace Owin.Security.Providers.Shopify +{ + using Microsoft.Owin; + using Microsoft.Owin.Security; + using Microsoft.Owin.Security.Provider; + + /// + /// Provides context information to middleware providers. + /// + public class ShopifyReturnEndpointContext : ReturnEndpointContext + { + /// + /// Initializes a new instance of the class. + /// + /// OWIN environment. + /// The authentication ticket. + public ShopifyReturnEndpointContext(IOwinContext context, AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/ShopifyAuthenticationExtensions.cs b/Owin.Security.Providers/Shopify/ShopifyAuthenticationExtensions.cs new file mode 100644 index 0000000..41a3689 --- /dev/null +++ b/Owin.Security.Providers/Shopify/ShopifyAuthenticationExtensions.cs @@ -0,0 +1,45 @@ +namespace Owin.Security.Providers.Shopify +{ + using System; + + public static class ShopifyAuthenticationExtensions + { + /// + /// Use Shopify Shop OAuth authentication. + /// + /// Instance of . + /// Shopify overrided authentication options. + /// Returns instance of . + public static IAppBuilder UseShopifyAuthentication(this IAppBuilder app, ShopifyAuthenticationOptions options) + { + if (null == app) + { + throw new ArgumentNullException(nameof(app)); + } + + if (null == options) + { + throw new ArgumentNullException(nameof(options)); + } + + app.Use(typeof(ShopifyAuthenticationMiddleware), app, options); + return app; + } + + /// + /// Use Shopify Shop OAuth authentication with default authentication options. + /// + /// Instance of . + /// Shopify App - API key. + /// Shopify App - API secret. + /// Returns instance of . + public static IAppBuilder UseShopifyAuthentication(this IAppBuilder app, string apiKey, string apiSecret) + { + return app.UseShopifyAuthentication(new ShopifyAuthenticationOptions + { + ApiKey = apiKey, + ApiSecret = apiSecret + }); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/ShopifyAuthenticationHandler.cs b/Owin.Security.Providers/Shopify/ShopifyAuthenticationHandler.cs new file mode 100644 index 0000000..13eb78c --- /dev/null +++ b/Owin.Security.Providers/Shopify/ShopifyAuthenticationHandler.cs @@ -0,0 +1,230 @@ +namespace Owin.Security.Providers.Shopify +{ + using Microsoft.Owin.Infrastructure; + using Microsoft.Owin.Logging; + using Microsoft.Owin.Security; + using Microsoft.Owin.Security.Infrastructure; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Security.Claims; + using System.Threading.Tasks; + + public class ShopifyAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + public ShopifyAuthenticationHandler(HttpClient httpClient, ILogger logger) + { + this.httpClient = httpClient; + this.logger = logger; + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + try + { + string code = null; + string state = null; + + var query = Request.Query; + var values = query.GetValues("code"); + if (null != values && 1 == values.Count) + { + code = values[0]; + } + + values = query.GetValues("state"); + if (null != values && 1 == values.Count) + { + state = values[0]; + } + + properties = Options.StateDataFormat.Unprotect(state); + if (null == properties) + { + return null; + } + + //// OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties, logger)) + { + return new AuthenticationTicket(null, properties); + } + + var currentShopifyShopName = properties.Dictionary["ShopName"]; + if (string.IsNullOrWhiteSpace(currentShopifyShopName)) + { + return null; + } + + var requestPrefix = Request.Scheme + "://" + Request.Host; + var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + //// Build up the body for the token request + var body = new List> + { + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", redirectUri), + new KeyValuePair("client_id", Options.ApiKey), + new KeyValuePair("client_secret", Options.ApiSecret) + }; + + //// Request the token + var requestMessage = new HttpRequestMessage(HttpMethod.Post, string.Format(CultureInfo.CurrentCulture, Options.Endpoints.TokenEndpoint, currentShopifyShopName)); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + requestMessage.Content = new FormUrlEncodedContent(body); + var tokenResponse = await httpClient.SendAsync(requestMessage); + tokenResponse.EnsureSuccessStatusCode(); + var text = await tokenResponse.Content.ReadAsStringAsync(); + + //// Deserializes the token response + dynamic response = JsonConvert.DeserializeObject(text); + var accessToken = (string)response.access_token; + + //// Get the Shopify shop information + var shopRequest = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.CurrentCulture, Options.Endpoints.ShopInfoEndpoint, currentShopifyShopName) + "?access_token=" + Uri.EscapeDataString(accessToken)); + shopRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var shopResponse = await httpClient.SendAsync(shopRequest, Request.CallCancelled); + shopResponse.EnsureSuccessStatusCode(); + text = await shopResponse.Content.ReadAsStringAsync(); + var shopifyShop = JObject.Parse(text); + var context = new ShopifyAuthenticatedContext(Context, shopifyShop, accessToken) + { + Identity = new ClaimsIdentity(Options.AuthenticationType, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType) + }; + + if (!string.IsNullOrEmpty(context.Id)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); + } + + if (!string.IsNullOrEmpty(context.UserName)) + { + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, XmlSchemaString, Options.AuthenticationType)); + } + + if (!string.IsNullOrEmpty(context.Email)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType)); + } + + if (!string.IsNullOrEmpty(context.ShopName)) + { + context.Identity.AddClaim(new Claim("urn:shopify:shopname", context.ShopName, XmlSchemaString, Options.AuthenticationType)); + } + + context.Properties = properties; + await Options.Provider.Authenticated(context); + return new AuthenticationTicket(context.Identity, context.Properties); + } + catch (Exception exception) + { + logger.WriteError(exception.Message); + } + + return new AuthenticationTicket(null, properties); + } + + protected override Task ApplyResponseChallengeAsync() + { + if (Response.StatusCode != 401) + { + return Task.FromResult(null); + } + + var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + if (challenge == null) + { + return Task.FromResult(null); + } + + var baseUri = Request.Scheme + Uri.SchemeDelimiter + Request.Host + Request.PathBase; + var currentUri = baseUri + Request.Path + Request.QueryString; + var redirectUri = baseUri + Options.CallbackPath; + + var properties = challenge.Properties; + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = currentUri; + } + + //// OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + var scope = string.Join(",", Options.Scope); + var state = Options.StateDataFormat.Protect(properties); + var authorizationEndpoint = + string.Format(CultureInfo.CurrentCulture, Options.Endpoints.AuthorizationEndpoint, challenge.Properties.Dictionary["ShopName"]) + + "?client_id=" + Uri.EscapeDataString(Options.ApiKey) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&scope=" + Uri.EscapeDataString(scope) + + "&state=" + Uri.EscapeDataString(state); + + Response.Redirect(authorizationEndpoint); + return Task.FromResult(null); + } + + public override async Task InvokeAsync() + { + return await InvokeReplyPathAsync(); + } + + private async Task InvokeReplyPathAsync() + { + if (!Options.CallbackPath.HasValue || Options.CallbackPath != Request.Path) + { + return false; + } + + //// TODO: error responses (I have no idea what this error responses TODO means :o) + var ticket = await AuthenticateAsync(); + if (null == ticket) + { + logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new ShopifyReturnEndpointContext(Context, ticket) + { + SignInAsAuthenticationType = Options.SignInAsAuthenticationType, + RedirectUri = ticket.Properties.RedirectUri + }; + + await Options.Provider.ReturnEndpoint(context); + if (null != context.SignInAsAuthenticationType && null != context.Identity) + { + var grantIdentity = context.Identity; + if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) + { + grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType); + } + + Context.Authentication.SignIn(context.Properties, grantIdentity); + } + + if (context.IsRequestCompleted || null == context.RedirectUri) + { + return context.IsRequestCompleted; + } + + var redirectUri = context.RedirectUri; + if (null == context.Identity) + { + //// Add a redirect hint that sign-in failed in some way + redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied"); + } + + Response.Redirect(redirectUri); + context.RequestCompleted(); + return context.IsRequestCompleted; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/ShopifyAuthenticationMiddleware.cs b/Owin.Security.Providers/Shopify/ShopifyAuthenticationMiddleware.cs new file mode 100644 index 0000000..51a537f --- /dev/null +++ b/Owin.Security.Providers/Shopify/ShopifyAuthenticationMiddleware.cs @@ -0,0 +1,89 @@ +namespace Owin.Security.Providers.Shopify +{ + using Microsoft.Owin; + using Microsoft.Owin.Logging; + using Microsoft.Owin.Security; + using Microsoft.Owin.Security.DataHandler; + using Microsoft.Owin.Security.DataProtection; + using Microsoft.Owin.Security.Infrastructure; + using Properties; + using System; + using System.Globalization; + using System.Net.Http; + + public class ShopifyAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly HttpClient httpClient; + private readonly ILogger logger; + + public ShopifyAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, ShopifyAuthenticationOptions options) + : base(next, options) + { + if (string.IsNullOrWhiteSpace(Options.ApiKey)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ApiKey")); + } + + if (string.IsNullOrWhiteSpace(Options.ApiSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ApiSecret")); + } + + this.logger = app.CreateLogger(); + if (null == Options.Provider) + { + Options.Provider = new ShopifyAuthenticationProvider(); + } + + if (null == Options.StateDataFormat) + { + var dataProtector = app.CreateDataProtector(typeof(ShopifyAuthenticationMiddleware).FullName, Options.AuthenticationType, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + if (string.IsNullOrWhiteSpace(Options.SignInAsAuthenticationType)) + { + Options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + + this.httpClient = new HttpClient(ResolveHttpMessageHandler(Options)) + { + Timeout = Options.BackchannelTimeout, + MaxResponseContentBufferSize = 1024 * 1024 * 10 + }; + + this.httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft Owin Shopify middleware"); + this.httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new ShopifyAuthenticationHandler(httpClient, logger); + } + + private static HttpMessageHandler ResolveHttpMessageHandler(ShopifyAuthenticationOptions options) + { + var handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); + + //// If they provided a validator, apply it or fail. + if (null == options.BackchannelCertificateValidator) + { + return handler; + } + + //// Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (null == webRequestHandler) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + return handler; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Shopify/ShopifyAuthenticationOptions.cs b/Owin.Security.Providers/Shopify/ShopifyAuthenticationOptions.cs new file mode 100644 index 0000000..1772384 --- /dev/null +++ b/Owin.Security.Providers/Shopify/ShopifyAuthenticationOptions.cs @@ -0,0 +1,129 @@ +namespace Owin.Security.Providers.Shopify +{ + using Microsoft.Owin; + using Microsoft.Owin.Security; + using System; + using System.Collections.Generic; + using System.Net.Http; + + public class ShopifyAuthenticationOptions : AuthenticationOptions + { + public class ShopifyAuthenticationEndpoints + { + /// + /// Endpoint which is used to redirect users to request Shopify shop access. + /// + /// Defaults to https://{shop}.myshopify.com/admin/oauth/authorize. + public string AuthorizationEndpoint { get; set; } + + /// + /// Endpoint which is used to exchange code for access token + /// + /// Defaults to https://{shop}.myshopify.com/admin/oauth/access_token. + public string TokenEndpoint { get; set; } + + /// + /// Endpoint which is used to obtain shop information after authentication + /// + /// Defaults to https://{shop}.myshopify.com/admin/shop. + public string ShopInfoEndpoint { get; set; } + } + + private const string DefaultAuthorizationEndPoint = "https://{0}.myshopify.com/admin/oauth/authorize"; + private const string DefaultTokenEndpoint = "https://{0}.myshopify.com/admin/oauth/access_token"; + private const string DefaultShopInfoEndpoint = "https://{0}.myshopify.com/admin/shop"; + + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used in back channel communications belong to Shopify. + /// + /// The pinned certificate validator. + /// If this property is null then the default certificate checks are performed, validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } + + /// + /// The HttpMessageHandler used to communicate with Shopify. This cannot be set at the same time as BackchannelCertificateValidator unless the value can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Shopify. + /// + /// The back channel timeout in milliseconds. + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The request path within the application's base path where the user-agent will be returned. The middleware will process this request when it arrives. Default value is "/signin-shopify". + /// + public PathString CallbackPath { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get + { + return Description.Caption; + } + + set + { + Description.Caption = value; + } + } + + /// + /// Gets or sets the Shopify app API key. + /// + public string ApiKey { get; set; } + + /// + /// Gets or sets the Shopify app API secret. + /// + public string ApiSecret { get; set; } + + /// + /// Gets the sets of OAuth endpoints used to authenticate against Shopify shop. + /// + public ShopifyAuthenticationEndpoints Endpoints { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public IShopifyAuthenticationProvider Provider { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a shop . + /// + public string SignInAsAuthenticationType { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ShopifyAuthenticationOptions() + : base("Shopify") + { + Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-shopify"); + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List { "read_content" }; + BackchannelTimeout = TimeSpan.FromSeconds(60); + Endpoints = new ShopifyAuthenticationEndpoints + { + AuthorizationEndpoint = DefaultAuthorizationEndPoint, + TokenEndpoint = DefaultTokenEndpoint, + ShopInfoEndpoint = DefaultShopInfoEndpoint + }; + } + } +} \ No newline at end of file diff --git a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs index 38f622f..e2ffd42 100755 --- a/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs +++ b/OwinOAuthProvidersDemo/App_Start/Startup.Auth.cs @@ -30,6 +30,7 @@ using Owin.Security.Providers.SoundCloud; using Owin.Security.Providers.Spotify; using Owin.Security.Providers.StackExchange; using Owin.Security.Providers.Steam; +using Owin.Security.Providers.Shopify; using Owin.Security.Providers.TripIt; using Owin.Security.Providers.Twitch; using Owin.Security.Providers.Untappd; @@ -53,7 +54,7 @@ namespace OwinOAuthProvidersDemo }); // Use a cookie to temporarily store information about a user logging in with a third party login provider app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - //app.UseDeviantArtAuthentication("id", "secret"); + //app.UseDeviantArtAuthentication("id", "secret"); //app.UseUntappdAuthentication("id", "secret"); // Uncomment the following lines to enable logging in with third party login providers //app.UseMicrosoftAccountAuthentication( @@ -185,6 +186,8 @@ namespace OwinOAuthProvidersDemo //}; //app.UseSalesforceAuthentication(salesforceOptions); + ////app.UseShopifyAuthentication("", ""); + //app.UseArcGISOnlineAuthentication( // clientId: "", // clientSecret: ""); @@ -201,7 +204,6 @@ namespace OwinOAuthProvidersDemo // clientId: "", // clientSecret: ""); - //app.UseBattleNetAuthentication(new BattleNetAuthenticationOptions //{ // ClientId = "", diff --git a/OwinOAuthProvidersDemo/Controllers/AccountController.cs b/OwinOAuthProvidersDemo/Controllers/AccountController.cs index 4e544a9..5813dca 100644 --- a/OwinOAuthProvidersDemo/Controllers/AccountController.cs +++ b/OwinOAuthProvidersDemo/Controllers/AccountController.cs @@ -182,15 +182,26 @@ namespace OwinOAuthProvidersDemo.Controllers return View(model); } + ////// + ////// POST: /Account/ExternalLogin + ////[HttpPost] + ////[AllowAnonymous] + ////[ValidateAntiForgeryToken] + ////public ActionResult ExternalLogin(string provider, string returnUrl) + ////{ + //// // Request a redirect to the external login provider + //// return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); + ////} + // // POST: /Account/ExternalLogin [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] - public ActionResult ExternalLogin(string provider, string returnUrl) + public ActionResult ExternalLogin(string provider, string returnUrl, string shopName = "") { // Request a redirect to the external login provider - return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); + return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }), null, shopName); } // @@ -224,10 +235,10 @@ namespace OwinOAuthProvidersDemo.Controllers // POST: /Account/LinkLogin [HttpPost] [ValidateAntiForgeryToken] - public ActionResult LinkLogin(string provider) + public ActionResult LinkLogin(string provider, string shopName) { // Request a redirect to the external login provider to link a login for the current user - return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account"), User.Identity.GetUserId()); + return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account"), User.Identity.GetUserId(), shopName); } // @@ -324,6 +335,8 @@ namespace OwinOAuthProvidersDemo.Controllers #region Helpers // Used for XSRF protection when adding external logins private const string XsrfKey = "XsrfId"; + // Used for Shopify external login to provide shopname while building endpoints. + private const string ShopNameKey = "ShopName"; private IAuthenticationManager AuthenticationManager { @@ -380,28 +393,36 @@ namespace OwinOAuthProvidersDemo.Controllers private class ChallengeResult : HttpUnauthorizedResult { - public ChallengeResult(string provider, string redirectUri) : this(provider, redirectUri, null) + public ChallengeResult(string provider, string redirectUri) : this(provider, redirectUri, null, null) { } - public ChallengeResult(string provider, string redirectUri, string userId) + public ChallengeResult(string provider, string redirectUri, string userId, string shopName) { LoginProvider = provider; RedirectUri = redirectUri; UserId = userId; + ShopName = shopName; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public string UserId { get; set; } + public string ShopName { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; if (UserId != null) { - properties.Dictionary[XsrfKey] = UserId; + properties.Dictionary[XsrfKey] = this.UserId; } + + if (!string.IsNullOrWhiteSpace(this.ShopName)) + { + properties.Dictionary[ShopNameKey] = this.ShopName; + } + context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); } } diff --git a/OwinOAuthProvidersDemo/Views/Account/_ExternalLoginsListPartial.cshtml b/OwinOAuthProvidersDemo/Views/Account/_ExternalLoginsListPartial.cshtml index fb306c4..47cf847 100644 --- a/OwinOAuthProvidersDemo/Views/Account/_ExternalLoginsListPartial.cshtml +++ b/OwinOAuthProvidersDemo/Views/Account/_ExternalLoginsListPartial.cshtml @@ -24,6 +24,10 @@

@foreach (AuthenticationDescription p in loginProviders) { + if (p.AuthenticationType.Equals("Shopify")) + { + @Html.TextBox("shopName") + } }

diff --git a/README.md b/README.md index 83ce2da..d657717 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - PayPal - Reddit - Salesforce + - Shopify - Slack - SoundCloud - Spotify @@ -41,7 +42,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - Wargaming ## Implementation Guides -For guides on how to implement these providers, please visit my blog, [Be a Big Rockstar](http://www.beabigrockstar.com). +For above listed provider implementation guide, visit Jerrie Pelser's blog - [Be a Big Rockstar](http://www.beabigrockstar.com) ## Installation To use these providers you will need to install the ```Owin.Security.Providers``` NuGet package. @@ -71,6 +72,7 @@ A big thanks goes out to all these contributors without whom this would not have * Anthony Ruffino (https://github.com/AnthonyRuffino) * Tommy Parnell (https://github.com/tparnell8) * Maxime Roussin-Bélanger (https://github.com/Lorac) +* Jaspalsinh Chauhan (https://github.com/jsinh) For most accurate and up to date list of contributors please see https://github.com/RockstarLabs/OwinOAuthProviders/graphs/contributors diff --git a/license.txt b/license.txt index 4d7326e..c11822f 100644 --- a/license.txt +++ b/license.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Jerrie Pelser +Copyright (c) 2014, 2015 Jerrie Pelser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal