diff --git a/NuGet/Owin.Security.Providers.nuspec b/NuGet/Owin.Security.Providers.nuspec index 13e95b7..1db3b1b 100644 --- a/NuGet/Owin.Security.Providers.nuspec +++ b/NuGet/Owin.Security.Providers.nuspec @@ -2,7 +2,7 @@ Owin.Security.Providers - 1.22.1 + 1.23 Jerrie Pelser and contributors Jerrie Pelser http://opensource.org/licenses/MIT @@ -19,16 +19,11 @@ Also adds generic OpenID 2.0 providers as well implementations for Steam and Wargaming. - Version 1.22 - - Enhancement - - Ability to specify prompt parameter for Salesforce provider - - Version 1.22 - + Version 1.23 + Added - - Added Imgur - - Added Backlog + - Added Shopify + - Added Cosign Copyright 2013, 2014 owin katana oauth LinkedIn Yahoo Google+ GitHub Reddit Instagram StackExchange SalesForce TripIt Buffer ArcGIS Dropbox Wordpress Battle.NET Yammer OpenID Steam Twitch diff --git a/Owin.Security.Providers/Backlog/BacklogAuthenticationHandler.cs b/Owin.Security.Providers/Backlog/BacklogAuthenticationHandler.cs index 07f91c3..40cad0f 100644 --- a/Owin.Security.Providers/Backlog/BacklogAuthenticationHandler.cs +++ b/Owin.Security.Providers/Backlog/BacklogAuthenticationHandler.cs @@ -11,6 +11,7 @@ using Microsoft.Owin.Security.Infrastructure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Owin.Security.Providers.Backlog; +using System.Net.Http.Headers; namespace Owin.Security.Providers.Backlog { @@ -72,9 +73,12 @@ namespace Owin.Security.Providers.Backlog body.Add(new KeyValuePair("client_secret", Options.ClientSecret)); // Get token - httpClient.DefaultRequestHeaders.Authorization = null; - HttpResponseMessage tokenResponse = - await httpClient.PostAsync(Options.TokenEndpoint, new FormUrlEncodedContent(body)); + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); + tokenRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + tokenRequest.Content = new FormUrlEncodedContent(body); + + HttpResponseMessage tokenResponse = await httpClient.SendAsync(tokenRequest, Request.CallCancelled); + tokenResponse.EnsureSuccessStatusCode(); string text = await tokenResponse.Content.ReadAsStringAsync(); @@ -88,12 +92,13 @@ namespace Owin.Security.Providers.Backlog string tokenType = (string)response.token_type; // Get the Backlog user - httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(tokenType, Uri.EscapeDataString(accessToken)); - HttpResponseMessage graphResponse = await httpClient.GetAsync( - Options.UserInfoEndpoint, Request.CallCancelled); + var userRequest = new HttpRequestMessage(HttpMethod.Get, Options.UserInfoEndpoint); + userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + userRequest.Headers.Authorization = new AuthenticationHeaderValue(tokenType, Uri.EscapeDataString(accessToken)); + HttpResponseMessage userResponse = await httpClient.SendAsync(userRequest, Request.CallCancelled); - graphResponse.EnsureSuccessStatusCode(); - text = await graphResponse.Content.ReadAsStringAsync(); + userResponse.EnsureSuccessStatusCode(); + text = await userResponse.Content.ReadAsStringAsync(); JObject user = JObject.Parse(text); var context = new BacklogAuthenticatedContext(Context, user, accessToken, expires, refreshToken); diff --git a/Owin.Security.Providers/Cosign/Constants.cs b/Owin.Security.Providers/Cosign/Constants.cs new file mode 100644 index 0000000..e582300 --- /dev/null +++ b/Owin.Security.Providers/Cosign/Constants.cs @@ -0,0 +1,7 @@ +namespace Owin.Security.Providers.Cosign +{ + internal static class Constants + { + public const string DefaultAuthenticationType = "Cosign"; + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/CosignAuthenticationExtensions.cs b/Owin.Security.Providers/Cosign/CosignAuthenticationExtensions.cs new file mode 100644 index 0000000..a8c0b0d --- /dev/null +++ b/Owin.Security.Providers/Cosign/CosignAuthenticationExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Owin.Security.Providers.Cosign +{ + public static class CosignAuthenticationExtensions + { + public static IAppBuilder UseCosignAuthentication(this IAppBuilder app, + CosignAuthenticationOptions options) + { + if (app == null) + throw new ArgumentNullException("app"); + if (options == null) + throw new ArgumentNullException("options"); + + app.Use(typeof(CosignAuthenticationMiddleware), app, options); + + return app; + } + + public static IAppBuilder UseCosignAuthentication(this IAppBuilder app, string clientId, string clientSecret) + { + return app.UseCosignAuthentication(new CosignAuthenticationOptions + { + + }); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/CosignAuthenticationHandler.cs b/Owin.Security.Providers/Cosign/CosignAuthenticationHandler.cs new file mode 100644 index 0000000..cf6dca3 --- /dev/null +++ b/Owin.Security.Providers/Cosign/CosignAuthenticationHandler.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Owin.Security.Providers.Cosign.Provider; + +namespace Owin.Security.Providers.Cosign +{ + public class CosignAuthenticationHandler : AuthenticationHandler + { + /* + Cosign sends authenticated users to iis web site root (not to application). + We need redirect user back to Identity Server application. + This can be done with different approaches http handler, url rewrite... + Here is UrlRewrite configuration + + + + + + + + + + + + + + + + + + + + */ + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private readonly ILogger logger; + + public CosignAuthenticationHandler(ILogger logger) + { + + this.logger = logger; + } + + protected override Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + + try + { + string serviceCookieValue = null; + string state = null; + + /*BUG: IReadableStringCollection has a bug. Some charactes can be missed in the collection and replaces with blank space. + Example: having "x" character in QueryString will result in having " " in the collection. + I will use QueryString from Request object instead of IReadableStringCollection*/ + + //IReadableStringCollection query = Request.Query; + //IList values = query.GetValues("cosign-" + Options.ClientServer); + //if (values != null && values.Count == 1) + //{ + // serviceCookieValue = values[0]; + //} + //values = query.GetValues("state"); + //if (values != null && values.Count == 1) + //{ + // state = values[0]; + //} + + string queryString = Request.QueryString.Value; + string[] values = queryString.Split(new string[] {"&"}, StringSplitOptions.RemoveEmptyEntries); + serviceCookieValue = + values.First(a => a.Contains(Options.ClientServer)) + .Replace("cosign-" + Options.ClientServer + "=", ""); + state = + values.First(a => a.Contains("state")) + .Replace("state=", ""); + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) + { + return null; + } + + + //// OAuth2 10.12 CSRF + //if (!ValidateCorrelationId(properties, logger)) + //{ + // return new AuthenticationTicket(null, properties); + //} + + + // Get host related information. + IPHostEntry hostEntry = Dns.GetHostEntry(Options.CosignServer); + + // Loop through the AddressList to obtain the supported AddressFamily. This is to avoid + // an exception that occurs when the host IP Address is not compatible with the address family + // (typical in the IPv6 case). + foreach (IPAddress address in hostEntry.AddressList) + { + IPEndPoint ipLocalEndPoint = new IPEndPoint(address, Options.CosignServicePort); + + using (TcpClient tcpClient = new TcpClient()) + { + + tcpClient.Connect(address, Options.CosignServicePort); + if (tcpClient.Connected) + { + logger.WriteInformation("Cosign authenticaion handler. Connected to server ip: " + address); + + //read message from connected server and validate response + NetworkStream networkStream = tcpClient.GetStream(); + byte[] buffer = new byte[256]; + var bytesRead = networkStream.ReadAsync(buffer, 0, buffer.Length); + var receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead.Result); + //expected message: 220 2 Collaborative Web Single Sign-On [COSIGNv3 FACTORS=5 REKEY] + if (receivedData.Substring(0, 3) != "220") + continue; + + //initiate secure negotiation and validate resonse + //buffer = Encoding.UTF8.GetBytes("STARTTLS 2\r\n"); + buffer = Encoding.UTF8.GetBytes("STARTTLS 2" + Environment.NewLine); + networkStream.Write(buffer, 0, buffer.Length); + networkStream.Flush(); + buffer = new byte[256]; + bytesRead = networkStream.ReadAsync(buffer, 0, buffer.Length); + receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead.Result); + //expected message: 220 Ready to start TLS + if (receivedData.Substring(0, 3) != "220") + continue; + + + SslStream sslStream = new SslStream(tcpClient.GetStream(), false, ValidateServerCertificate, + null); + + X509CertificateCollection certs = GetCertificateCertificateCollection(Options.ClientServer, + StoreName.My, + StoreLocation.LocalMachine); + try + { + Task authResult = sslStream.AuthenticateAsClientAsync(Options.CosignServer, certs, SslProtocols.Tls, false); + authResult.GetAwaiter().GetResult(); + } + catch (AuthenticationException e) + { + logger.WriteError(e.Message); + if (e.InnerException != null) + { + logger.WriteError(string.Format("Inner exception: {0}", e.InnerException.Message)); + } + logger.WriteError("Authentication failed - closing the connection."); + tcpClient.Close(); + continue; + } + catch (Exception ex) + { + logger.WriteError(ex.Message); + tcpClient.Close(); + continue; + } + + if (!sslStream.IsEncrypted || !sslStream.IsSigned || !sslStream.IsMutuallyAuthenticated) + continue; + // The server name must match the name on the server certificate. + if (!sslStream.IsAuthenticated) + continue; + + + buffer = new byte[256]; + bytesRead = sslStream.ReadAsync(buffer, 0, buffer.Length); + receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead.Result); + //expected message: 220 2 Collaborative Web Single Sign-On [COSIGNv3 FACTORS=5 REKEY] + if (receivedData.Substring(0, 3) != "220") + continue; + + byte[] data = + Encoding.UTF8.GetBytes("CHECK " + "cosign-" + Options.ClientServer + "=" + + serviceCookieValue + Environment.NewLine); + + sslStream.Write(data, 0, data.Length); + sslStream.Flush(); + buffer = new byte[256]; + + bytesRead = sslStream.ReadAsync(buffer, 0, buffer.Length); + receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead.Result); + + + switch (receivedData.Substring(0, 1)) + { + case "2": + //Success + logger.WriteInformation("Cosign authenticaion handler. 2-Response from Server: Success."); + var context = new CosignAuthenticatedContext(Context, receivedData) + { + Identity = new ClaimsIdentity( + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType) + }; + + + var identity = new ClaimsIdentity(Options.SignInAsAuthenticationType); + if (!string.IsNullOrEmpty(context.Id)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, + XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.UserId)) + { + identity.AddClaim(new Claim("UserId", context.UserId, XmlSchemaString, + Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.IpAddress)) + { + identity.AddClaim(new Claim("IpAddress", context.IpAddress, XmlSchemaString, + Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Realm)) + { + identity.AddClaim(new Claim("Realm", context.Realm, XmlSchemaString, + Options.AuthenticationType)); + } + + context.Properties = properties; + + return Task.FromResult(new AuthenticationTicket(identity, properties)); + + + case "4": + //Logged out + logger.WriteInformation("Cosign authenticaion handler. Response from Server: 4-Logged out."); + break; + case "5": + //Try a different server + logger.WriteInformation("Cosign authenticaion handler. Response from Server: 5-Try different server."); + break; + default: + logger.WriteInformation("Cosign authenticaion handler. Response from Server: Undefined."); + break; + + } + } + } + } + } + catch (Exception ex) + { + logger.WriteError(ex.Message); + } + + + return Task.FromResult(new AuthenticationTicket(null, properties)); + } + + protected override Task ApplyResponseChallengeAsync() + { + if (Response.StatusCode == 401) + { + var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); + + // Only react to 401 if there is an authentication challenge for the authentication + // type of this handler. + if (challenge != null) + { + var state = challenge.Properties; + + if (string.IsNullOrEmpty(state.RedirectUri)) + { + state.RedirectUri = Request.Uri.ToString(); + } + + var stateString = Options.StateDataFormat.Protect(state); + + string loginUrl = + "https://" + Options.CosignServer + "/?cosign-" + Options.ClientServer + + "&state=" + Uri.EscapeDataString(stateString) + + "&core=" + Options.IdentityServerHostInstance; + logger.WriteInformation("Cosign authenticaion handler. Redirecting to cosign. " + loginUrl); + Response.Redirect(loginUrl); + } + } + + return Task.FromResult(null); + } + + public override async Task InvokeAsync() + { + // This is always invoked on each request. For passive middleware, only do anything if this is + // for our callback path when the user is redirected back from the authentication provider. + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + var ticket = await AuthenticateAsync(); + + if (ticket != null) + { + Context.Authentication.SignIn(ticket.Properties, ticket.Identity); + + Response.Redirect(ticket.Properties.RedirectUri); + + // Prevent further processing by the owin pipeline. + return true; + } + } + // Let the rest of the pipeline run. + return false; + } + + + public static X509CertificateCollection GetCertificateCertificateCollection(string subjectName, + StoreName storeName, + StoreLocation storeLocation) + { + // The following code gets the cert from the keystore + X509Store store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadOnly); + X509Certificate2Collection certCollection = + store.Certificates.Find(X509FindType.FindBySubjectName, + subjectName, + false); + return certCollection; + } + + private static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/CosignAuthenticationMiddleware.cs b/Owin.Security.Providers/Cosign/CosignAuthenticationMiddleware.cs new file mode 100644 index 0000000..d0af3a9 --- /dev/null +++ b/Owin.Security.Providers/Cosign/CosignAuthenticationMiddleware.cs @@ -0,0 +1,59 @@ +using System; +using System.Globalization; +using System.Net.Http; +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 Owin.Security.Providers.Cosign.Provider; +using Owin.Security.Providers.Properties; + +namespace Owin.Security.Providers.Cosign +{ + public class CosignAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger logger; + public CosignAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, CosignAuthenticationOptions options) + : base(next, options) + { + if (String.IsNullOrWhiteSpace(Options.ClientServer)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "ClientServer")); + if (String.IsNullOrWhiteSpace(Options.CosignServer)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "CosignServer")); + if ((Options.CosignServicePort==0)) + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + Resources.Exception_OptionMustBeProvided, "CosignServicePort")); + + + logger = app.CreateLogger(); + logger.WriteInformation("CosignAthenticationMiddleware has been created"); + if (Options.Provider == null) + Options.Provider = new CosignAuthenticationProvider(); + + if (string.IsNullOrEmpty(Options.SignInAsAuthenticationType)) + { + options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + if (options.StateDataFormat == null) + { + var dataProtector = app.CreateDataProtector(typeof (CosignAuthenticationMiddleware).FullName, + options.AuthenticationType); + + options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + + + } + + + protected override AuthenticationHandler CreateHandler() + { + return new CosignAuthenticationHandler(logger); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/CosignAuthenticationOptions.cs b/Owin.Security.Providers/Cosign/CosignAuthenticationOptions.cs new file mode 100644 index 0000000..3d95137 --- /dev/null +++ b/Owin.Security.Providers/Cosign/CosignAuthenticationOptions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net.Http; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Owin.Security.Providers.Cosign.Provider; + +namespace Owin.Security.Providers.Cosign +{ + public class CosignAuthenticationOptions : AuthenticationOptions + { + + + //full login url https://weblogin.umich.edu/?cosign-www.kbv.law.umich.edu&http://www.kbv.law.umich.edu/IdentityServer/core1 + //return url https://www.kbv.law.umich.edu/cosign/valid?cosign-www.kbv.law.umich.edu=COSIGN_VALUE&http://www.kbv.law.umich.edu/IdentityServer/core1 + //private const string LoginEndPoint = "https://weblogin.umich.edu/?"; + //private const string cosignServer = "weblogin.umich.edu"; + //private const string clientServer = "www.kbv.law.umich.edu"; + + + + + + /// + /// 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-cosign". + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the Cosgin server + /// + public string CosignServer { get; set; } + + /// + /// Gets or sets the instance of Identity Server Host + /// + public string IdentityServerHostInstance { get; set; } + + + /// + /// Gets or sets the Cosign service name + /// + public string ClientServer { get; set; } + + /// + /// Gets or sets the Cosign service port + /// + public int CosignServicePort { get; set; } + + /// + /// Gets or sets the used in the authentication events + /// + public ICosignAuthenticationProvider Provider { get; set; } + + + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user + /// . + /// + 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 + /// + public CosignAuthenticationOptions(): base("Cosign") + { + //CosignServer = cosignServer; + //ClientServer = clientServer; + Description.Caption = Constants.DefaultAuthenticationType; + CallbackPath = new PathString("/signin-cosign"); + AuthenticationMode = AuthenticationMode.Passive; + IdentityServerHostInstance = ""; + + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/Provider/CosignAuthenticatedContext.cs b/Owin.Security.Providers/Cosign/Provider/CosignAuthenticatedContext.cs new file mode 100644 index 0000000..7b71e82 --- /dev/null +++ b/Owin.Security.Providers/Cosign/Provider/CosignAuthenticatedContext.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Specialized; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.Cosign.Provider +{ + /// + /// Contains information about the login session as well as the user . + /// + public class CosignAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The OWIN environment + /// Response from Cosign server + public CosignAuthenticatedContext(IOwinContext context, string cosignResponse): base(context) + { + CosignResposponse = cosignResponse; + string[] returnedData = CosignResposponse.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries); + Id = TryGetValue(returnedData, "id"); + UserId = TryGetValue(returnedData, "userid"); + IpAddress = TryGetValue(returnedData, "ipaddress"); + Realm = TryGetValue(returnedData, "realm"); + } + + /// + /// Gets the Cosign response + /// + public string CosignResposponse { get; private set; } + + /// + /// Gets the Cosign ID + /// + public string Id { get; private set; } + + + /// + /// Gets the Cosign userId + /// + public string UserId { get; private set; } + + /// + /// Gets the representing the user identity + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets the representing the user ipaddress + /// + public string IpAddress { get; set; } + /// + /// Gets the representing the user realm + /// + public string Realm { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + + private static string TryGetValue(string[] cosignData, string propertyName) + { + + switch (propertyName.ToLower()) + { + case "ipaddress": + if (cosignData.GetUpperBound(0)>=0) + return cosignData[1]; + return ""; + case "userid": + if (cosignData.GetUpperBound(0) >= 1) + return cosignData[2]; + return ""; + case "id": + if (cosignData.GetUpperBound(0) >= 1) + return sha256_hash( cosignData[2]); + return ""; + case "realm": + if (cosignData.GetUpperBound(0) >=2) + return cosignData[3].Trim(new char[]{ Environment.NewLine.ToCharArray()[0]} ); + return ""; + default: + return ""; + } + + } + + + private static string sha256_hash(string value) + { + StringBuilder Sb = new StringBuilder(); + + using (SHA256 hash = SHA256.Create()) + { + Encoding enc = Encoding.UTF8; + byte[] result = hash.ComputeHash(enc.GetBytes(value)); + + foreach (byte b in result) + Sb.Append(b.ToString("x2")); + } + + return Sb.ToString(); + } + } +} diff --git a/Owin.Security.Providers/Cosign/Provider/CosignAuthenticationProvider.cs b/Owin.Security.Providers/Cosign/Provider/CosignAuthenticationProvider.cs new file mode 100644 index 0000000..c6bba0b --- /dev/null +++ b/Owin.Security.Providers/Cosign/Provider/CosignAuthenticationProvider.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Cosign.Provider +{ + /// + /// Default implementation. + /// + public class CosignAuthenticationProvider : ICosignAuthenticationProvider + { + /// + /// Initializes a + /// + public CosignAuthenticationProvider() + { + 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 Cosign succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(CosignAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(CosignReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Cosign/Provider/CosignReturnEndpointContext.cs b/Owin.Security.Providers/Cosign/Provider/CosignReturnEndpointContext.cs new file mode 100644 index 0000000..d626296 --- /dev/null +++ b/Owin.Security.Providers/Cosign/Provider/CosignReturnEndpointContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Provider; + +namespace Owin.Security.Providers.Cosign.Provider +{ + /// + /// Provides context information to middleware providers. + /// + public class CosignReturnEndpointContext : ReturnEndpointContext + { + /// + /// + /// + /// OWIN environment + /// The authentication ticket + public CosignReturnEndpointContext( + IOwinContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/Owin.Security.Providers/Cosign/Provider/ICosignAuthenticationProvider.cs b/Owin.Security.Providers/Cosign/Provider/ICosignAuthenticationProvider.cs new file mode 100644 index 0000000..68cadb8 --- /dev/null +++ b/Owin.Security.Providers/Cosign/Provider/ICosignAuthenticationProvider.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Owin.Security.Providers.Cosign.Provider +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface ICosignAuthenticationProvider + { + /// + /// Invoked whenever Cosign succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(CosignAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(CosignReturnEndpointContext context); + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index 4688131..8651f58 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -274,6 +274,15 @@ + + + + + + + + + diff --git a/Owin.Security.Providers/Properties/AssemblyInfo.cs b/Owin.Security.Providers/Properties/AssemblyInfo.cs index 06fcae2..19a04da 100644 --- a/Owin.Security.Providers/Properties/AssemblyInfo.cs +++ b/Owin.Security.Providers/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.22.1.0")] -[assembly: AssemblyFileVersion("1.22.1.0")] +[assembly: AssemblyVersion("1.23.0.0")] +[assembly: AssemblyFileVersion("1.23.0.0")] 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..2bc6b35 --- /dev/null +++ b/Owin.Security.Providers/Shopify/Provider/ShopifyAuthenticatedContext.cs @@ -0,0 +1,85 @@ +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) + { + if (shop != null && shop.First != null && shop.First.First != null) + { + var propertyValue = shop.First.First[propertyName]; + + if (propertyValue != null) + return propertyValue.ToString(); + } + + return null; + } + } +} \ 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..fe74719 --- /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("app"); + } + + if (null == options) + { + throw new ArgumentNullException("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 468d91e..fd7e560 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; @@ -54,7 +55,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( @@ -186,6 +187,8 @@ namespace OwinOAuthProvidersDemo //}; //app.UseSalesforceAuthentication(salesforceOptions); + ////app.UseShopifyAuthentication("", ""); + //app.UseArcGISOnlineAuthentication( // clientId: "", // clientSecret: ""); @@ -202,7 +205,6 @@ namespace OwinOAuthProvidersDemo // clientId: "", // clientSecret: ""); - //app.UseBattleNetAuthentication(new BattleNetAuthenticationOptions //{ // ClientId = "", @@ -273,11 +275,22 @@ namespace OwinOAuthProvidersDemo //app.UseBacklogAuthentication(options); -// app.UseFitbitAuthentication(new FitbitAuthenticationOptions -// { -// ClientId = "", -// ClientSecret = "" -// }); + //var cosignOptions = new CosignAuthenticationOptions + //{ + // AuthenticationType = "Cosign", + // SignInAsAuthenticationType = signInAsType, + // CosignServer = "weblogin.umich.edu", + // CosignServicePort = 6663, + // IdentityServerHostInstance = "core1", + // ClientServer = "cosignservername" + //}; + //app.UseCosignAuthentication(cosignOptions); + + //app.UseFitbitAuthentication(new FitbitAuthenticationOptions + //{ + // ClientId = "", + // ClientSecret = "" + //}); } } } 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 c39e2c1..503566e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - Backlog - Battle.net - Buffer + - Cosign - DeviantArt - Dropbox - EVEOnline @@ -25,6 +26,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http - PayPal - Reddit - Salesforce + - Shopify - Slack - SoundCloud - Spotify @@ -42,7 +44,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. @@ -72,6 +74,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