using System; using System.Net; using System.Threading; using System.Collections.Specialized; using ExtremeSwank.Authentication.OpenID.Persistence; namespace ExtremeSwank.Authentication.OpenID { /// /// Provides OpenID client support for desktop applications. /// /// /// /// The user's web browser will need to be launched to perform authentication. /// All data will be passed back to the application through an embedded web server that will /// only start as needed to receive the authentication response. /// /// /// Use BeginAuthentication() to asynchronously start /// the temporary HTTP server and return the redirect URL. This URL should be passed to the client host /// and launched in its web browser. Use GetAuthenticationResponse() to wait for the authentication /// response, or subscribe to the available events if blocking is not desired. /// /// /// In either case, the HTTP server will be closed if the timeout has expired. /// /// public class OpenIDDesktopConsumer { #region Fields and Properties HttpListener listener; OpenIDConsumer openid; bool responsereceived; bool authSuccessful; Errors _ErrorState; int _Timeout; string _Identity; int _Port; bool _UseRandomPort; string _Hostname; DateTime startTime; ISessionPersistence _SessionManager; IAssociationPersistence _AssociationManager; string _DirectedOpenIDServer; bool _ReturnEmptyHTML = false; /// /// Generates a URL prefix that will be used to initialize /// the temporary HTTP server. /// string Prefix { get { return "http://*:" + _Port + "/"; } } /// /// The OpenIDConsumer object used for processing. /// public OpenIDConsumer Consumer { get { return openid; } } /// /// Current error status. /// public Errors Error { get { return _ErrorState; } } /// /// The temporary HTTP server's result HTML is original(false) or non-script version(true). /// public bool ReturnEmptyHTML { get { return _ReturnEmptyHTML; } set { _ReturnEmptyHTML = value; } } #endregion #region Constructors /// /// Create a new instance of OpenIDDesktopConsumer. /// /// The OpenID Identity to authenticate. /// The amount of time to wait for a response back from the OpenID Provider. /// The hostname that will be used for the return URL. /// The port number that should be used to receive the authentication response from the User's web browser, if random ports are not desired. public OpenIDDesktopConsumer(string identity, int timeout, string hostname, int port) { _UseRandomPort = false; Init(identity); _Hostname = hostname; _Timeout = timeout; _Port = port; } /// /// Create a new instance of OpenIDDesktopConsumer. /// /// The OpenID Identity to authenticate. /// The amount of time to wait for a response back from the OpenID Provider. /// The hostname that will be used for the return URL. public OpenIDDesktopConsumer(string identity, int timeout, string hostname) { _UseRandomPort = true; Init(identity); _Hostname = hostname; _Timeout = timeout; } /// /// Create a new instance of OpenIDDesktopConsumer. /// /// The OpenID Identity to authenticate. /// The amount of time to wait for a response back from the OpenID Provider. public OpenIDDesktopConsumer(string identity, int timeout) { _UseRandomPort = true; Init(identity); _Timeout = timeout; } #endregion #region Public Methods /// /// Start listening for an authentication response, and return the redirect URL that the web browser /// will be pointed to. /// /// The redirect URL for the remote web browser. public string BeginAuthentication() { NonblockingListener(); string url = openid.BeginAuth(false, false); if (String.IsNullOrEmpty(url)) { _ErrorState = openid.GetError(); CancelAuthentication(); } else { AuthenticationTimer at = new AuthenticationTimer(WaitForResponse); at.BeginInvoke(null, null); } return url; } /// /// Stop listening for the authentication response. /// public void CancelAuthentication() { if (listener != null) { listener.Abort(); listener = null; } } /// /// Wait until an authentication response has been received, and return the result. /// /// True if succeeded, false if not. public bool GetAuthenticationResponse() { return BlockWaitForResponse(); } /// /// Enable Stateful authentication using supplied persistence objects. /// /// IAssociationPersistence object to use when persisting associations. /// ISessionPersistence object to use when persisting per-user data. public void EnableStatefulMode(IAssociationPersistence assocmgr, ISessionPersistence sessionmgr) { _AssociationManager = assocmgr; _SessionManager = sessionmgr; openid = GetConsumer(new NameValueCollection()); } /// /// Enable Stateful authentication using default (volatile) persistence objects. /// /// /// Uses and /// objects. These are volatile, and all contained data will be destroyed when /// the object is disposed. /// public void EnableStatefulMode() { _AssociationManager = new SingularAssociationManager(); _SessionManager = new SingularSessionManager(); openid = GetConsumer(new NameValueCollection()); } /// /// Enable Directed Identity mode for a specific OpenID Provider. /// /// /// Consumer will only authenticate with the supplied provider URL, /// and will accept the OpenID returned in the authentication response. /// /// URL to the desired OpenID server public void EnableDirectedIdentity(string openidserver) { _DirectedOpenIDServer = openidserver; openid.UseDirectedIdentity = true; openid.OpenIDServer = openidserver; } #endregion #region Private Methods and Properties /// /// Shared initialization method used by all constructors. /// /// OpenID Identity to authenticate. void Init(string identity) { GenerateRandomPort(); authSuccessful = false; responsereceived = false; _Identity = identity; _Hostname = System.Environment.MachineName; openid = GetConsumer(new NameValueCollection()); } /// /// Create a new OpenIDConsumer object with settings appropriate for this class. /// /// The containing the received arguments. /// The created OpenIDConsumer object. OpenIDConsumer GetConsumer(NameValueCollection arguments) { openid = new OpenIDConsumer(arguments, _AssociationManager, _SessionManager); openid.Identity = _Identity; if (!String.IsNullOrEmpty(_DirectedOpenIDServer)) { openid.UseDirectedIdentity = true; openid.OpenIDServer = _DirectedOpenIDServer; } SetConsumerURLs(); return openid; } /// /// Updates the object with the /// correct TrustRoot and RetunURL values. /// void SetConsumerURLs() { if (openid != null) { openid.TrustRoot = "http://" + _Hostname.ToLower() + ":" + _Port + "/"; openid.ReturnURL = openid.TrustRoot + "return"; } } /// /// If a port was not specified in the constructor, generate a /// random port number between 1024 and 5000. /// void GenerateRandomPort() { if (_UseRandomPort) { Random r = new Random(DateTime.Now.Second); _Port = r.Next(1024, 5000); SetConsumerURLs(); } } /// /// Intended to be called asynchronously. Check every second to see if the user's authentication /// response has been received. If timeout occurs, automatically close HTTP server. /// /// True if a response has been received, false if timed out. bool WaitForResponse() { while (true) { if (responsereceived) { break; } if (DateTime.Now > startTime.AddSeconds(_Timeout)) { _ErrorState = Errors.SessionTimeout; authSuccessful = false; CancelAuthentication(); if (AuthenticationResponseTimedOut != null) { AuthenticationResponseTimedOut.Invoke(this, new EventArgs()); } break; } Thread.Sleep(1000); } return authSuccessful; } /// /// Intended to be called synchronously. Check every second to see if the user's authentication /// response has been received. Will wait until the automatically called asynchronous WaitForResponse() /// function has returned. /// /// bool BlockWaitForResponse() { while (true) { if (responsereceived) { break; } if (listener == null) { break; } if (!listener.IsListening) { break; } Thread.Sleep(1000); } return authSuccessful; } /// /// Start the temporary HTTP server, and register /// the callback method. /// void NonblockingListener() { bool success = false; int attempts = 0; IAsyncResult result; while (!success) { try { listener = new HttpListener(); listener.Prefixes.Add(Prefix); listener.Start(); startTime = DateTime.Now; success = true; result = listener.BeginGetContext(new AsyncCallback(ListenerCallback), listener); } catch { startTime = new DateTime(); GenerateRandomPort(); success = false; } attempts++; if (attempts >= 5) { throw new Exception("Could not open HTTP listening port after 5 attempts. Ensure the process is running with Administrator privileges."); } } } #endregion #region Private Event Callbacks /// /// The callback method for the temporary HTTP server. /// /// The object representing the active . void ListenerCallback(IAsyncResult result) { HttpListener listener = (HttpListener)result.AsyncState; try { // Call EndGetContext to complete the asynchronous operation. HttpListenerContext context = listener.EndGetContext(result); HttpListenerRequest request = context.Request; #if true // Create "request.QueryString" as UTF-8 NameValueCollection queryString = new NameValueCollection(); foreach (string query in request.Url.Query.Substring(1).Split('&')) { int pos = query.IndexOf('='); if (pos > 0) { string key = Uri.UnescapeDataString(query.Substring(0, pos)); string value = Uri.UnescapeDataString(query.Substring(pos + 1)); queryString.Add(key, value); } else { queryString.Add(null, Uri.UnescapeDataString(query.Substring(pos + 1))); } } #else openid = GetConsumer(request.QueryString); #endif openid = GetConsumer(queryString); openid.ValidationSucceeded += new EventHandler(openid_ValidationSucceeded); openid.ReceivedCancelResponse += new EventHandler(openid_ReceivedCancelResponse); openid.ValidationFailed += new EventHandler(openid_ValidationFailed); openid.HandleResponses(); // Obtain a response object. HttpListenerResponse response = context.Response; // Construct a response. string responseString; if (_ReturnEmptyHTML) { responseString = ""; } else { responseString = "Please close this browser window."; } byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString); // Get a response stream and write the response to it. response.ContentLength64 = buffer.Length; System.IO.Stream output = response.OutputStream; output.Write(buffer, 0, buffer.Length); // You must close the output stream. output.Close(); responsereceived = true; listener.Close(); if (AuthenticationResponseReceived != null) { AuthenticationResponseReceived.Invoke(this, new EventArgs()); } } catch { } } /// /// Callback method for when OpenIDConsumer invokes its event. /// /// The OpenIDConsumer object invoking the event. /// Event arguments. void openid_ReceivedCancelResponse(object sender, EventArgs e) { _ErrorState = Errors.RequestCancelled; if (AuthenticationCancelled != null) { AuthenticationCancelled.Invoke(this, new EventArgs()); } } /// /// Callback method for when OpenIDConsumer invokes its event. /// /// The OpenIDConsumer object invoking the event. /// Event arguments. void openid_ValidationSucceeded(object sender, EventArgs e) { authSuccessful = true; if (AuthenticationSuccessful != null) { AuthenticationSuccessful.Invoke(this, new EventArgs()); } } /// /// Callback method for when OpenIDConsumer invokes its event. /// /// The OpenIDConsumer object invoking the event. /// Event arguments. void openid_ValidationFailed(object sender, EventArgs e) { if (AuthenticationFailed != null) { AuthenticationFailed.Invoke(this, new EventArgs()); } } /// /// Delegate used for the asynchronous timeout checker. /// /// Value not used. delegate bool AuthenticationTimer(); #endregion #region Public Events /// /// Authentication response has been received. /// public event EventHandler AuthenticationResponseReceived; /// /// Authentication response has timed out. /// public event EventHandler AuthenticationResponseTimedOut; /// /// Authentication was successful. /// public event EventHandler AuthenticationSuccessful; /// /// Authentication failed. /// public event EventHandler AuthenticationFailed; /// /// Authentication was cancelled at the OpenID Provider. /// public event EventHandler AuthenticationCancelled; #endregion } }