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