Merge pull request #122 from kvoyk/master

Cosign provider for Identity Server
This commit is contained in:
Jerrie Pelser
2015-08-19 10:58:22 +07:00
11 changed files with 748 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
namespace Owin.Security.Providers.Cosign
{
internal static class Constants
{
public const string DefaultAuthenticationType = "Cosign";
}
}

View File

@@ -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
{
});
}
}
}

View File

@@ -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<CosignAuthenticationOptions>
{
/*
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
<rewrite>
<rules>
<clear />
<rule name="Cosign-RedirectCore1" enabled="true" stopProcessing="true">
<match url="cosign/valid?" negate="false" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
<add input="{QUERY_STRING}" pattern="core=core1" />
</conditions>
<action type="Redirect" url="https://yourserver/host/path/signin-cosign" redirectType="SeeOther" />
</rule>
<rule name="Cosign-RedirectCore2" enabled="true" stopProcessing="true">
<match url="cosign/valid?" negate="false" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
<add input="{QUERY_STRING}" pattern="core=core2" />
</conditions>
<action type="Redirect" url="https://yourserver/host/path/signin-cosign" redirectType="SeeOther" />
</rule>
</rules>
</rewrite>
*/
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<AuthenticationTicket> 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<string> 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<object>(null);
}
public override async Task<bool> 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;
}
}
}

View File

@@ -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<CosignAuthenticationOptions>
{
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<CosignAuthenticationMiddleware>();
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<CosignAuthenticationOptions> CreateHandler()
{
return new CosignAuthenticationHandler(logger);
}
}
}

View File

@@ -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";
/// <summary>
/// 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".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the Cosgin server
/// </summary>
public string CosignServer { get; set; }
/// <summary>
/// Gets or sets the instance of Identity Server Host
/// </summary>
public string IdentityServerHostInstance { get; set; }
/// <summary>
/// Gets or sets the Cosign service name
/// </summary>
public string ClientServer { get; set; }
/// <summary>
/// Gets or sets the Cosign service port
/// </summary>
public int CosignServicePort { get; set; }
/// <summary>
/// Gets or sets the <see cref="ICosignAuthenticationProvider" /> used in the authentication events
/// </summary>
public ICosignAuthenticationProvider Provider { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user
/// <see cref="System.Security.Claims.ClaimsIdentity" />.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
/// <summary>
/// Initializes a new <see cref="CosignAuthenticationOptions" />
/// </summary>
public CosignAuthenticationOptions(): base("Cosign")
{
//CosignServer = cosignServer;
//ClientServer = clientServer;
Description.Caption = Constants.DefaultAuthenticationType;
CallbackPath = new PathString("/signin-cosign");
AuthenticationMode = AuthenticationMode.Passive;
IdentityServerHostInstance = "";
}
}
}

View File

@@ -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
{
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class CosignAuthenticatedContext : BaseContext
{
/// <summary>
/// Initializes a <see cref="CosignAuthenticatedContext"/>
/// </summary>
/// <param name="context">The OWIN environment</param>
/// <param name="cosignResponse">Response from Cosign server</param>
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");
}
/// <summary>
/// Gets the Cosign response
/// </summary>
public string CosignResposponse { get; private set; }
/// <summary>
/// Gets the Cosign ID
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets the Cosign userId
/// </summary>
public string UserId { get; private set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user identity
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets the <see cref="IpAddress"/> representing the user ipaddress
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// Gets the <see cref="Realm"/> representing the user realm
/// </summary>
public string Realm { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties
/// </summary>
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();
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Threading.Tasks;
namespace Owin.Security.Providers.Cosign.Provider
{
/// <summary>
/// Default <see cref="ICosignAuthenticationProvider"/> implementation.
/// </summary>
public class CosignAuthenticationProvider : ICosignAuthenticationProvider
{
/// <summary>
/// Initializes a <see cref="CosignAuthenticationProvider"/>
/// </summary>
public CosignAuthenticationProvider()
{
OnAuthenticated = context => Task.FromResult<object>(null);
OnReturnEndpoint = context => Task.FromResult<object>(null);
}
/// <summary>
/// Gets or sets the function that is invoked when the Authenticated method is invoked.
/// </summary>
public Func<CosignAuthenticatedContext, Task> OnAuthenticated { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<CosignReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Invoked whenever Cosign succesfully authenticates a user
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task Authenticated(CosignAuthenticatedContext context)
{
return OnAuthenticated(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(CosignReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
}
}

View File

@@ -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
{
/// <summary>
/// Provides context information to middleware providers.
/// </summary>
public class CosignReturnEndpointContext : ReturnEndpointContext
{
/// <summary>
///
/// </summary>
/// <param name="context">OWIN environment</param>
/// <param name="ticket">The authentication ticket</param>
public CosignReturnEndpointContext(
IOwinContext context,
AuthenticationTicket ticket)
: base(context, ticket)
{
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Threading.Tasks;
namespace Owin.Security.Providers.Cosign.Provider
{
/// <summary>
/// Specifies callback methods which the <see cref="CosignAuthenticationMiddleware"></see> invokes to enable developer control over the authentication process. />
/// </summary>
public interface ICosignAuthenticationProvider
{
/// <summary>
/// Invoked whenever Cosign succesfully authenticates a user
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task Authenticated(CosignAuthenticatedContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(CosignReturnEndpointContext context);
}
}

View File

@@ -274,6 +274,16 @@ namespace OwinOAuthProvidersDemo
//app.UseBacklogAuthentication(options);
//var cosignOptions = new CosignAuthenticationOptions
//{
// AuthenticationType = "Cosign",
// SignInAsAuthenticationType = signInAsType,
// CosignServer = "weblogin.umich.edu",
// CosignServicePort = 6663,
// IdentityServerHostInstance = "core1",
// ClientServer = "cosignservername"
//};
//app.UseCosignAuthentication(cosignOptions);
}
}
}

View File

@@ -10,6 +10,7 @@ Provides a set of extra authentication providers for OWIN ([Project Katana](http
- Backlog
- Battle.net
- Buffer
- Cosign
- DeviantArt
- Dropbox
- EVEOnline