2003-12-15 Gonzalo Paniagua Javier <gonzalo@ximian.com>
authorGonzalo Paniagua Javier <gonzalo.mono@gmail.com>
Mon, 15 Dec 2003 04:59:00 +0000 (04:59 -0000)
committerGonzalo Paniagua Javier <gonzalo.mono@gmail.com>
Mon, 15 Dec 2003 04:59:00 +0000 (04:59 -0000)
* Mono.Http.Modules/AuthenticationModule.cs:
* Mono.Http.Modules/BasicAuthenticationModule.cs:
* Mono.Http.Modules/DigestAuthenticationModule.cs: new modules to do
Basic and Digest authentication.

* samples/auth.xml: sample user/password file for the modules.

svn path=/trunk/mcs/; revision=21131

mcs/class/Mono.Http/Mono.Http.Modules/AuthenticationModule.cs [new file with mode: 0644]
mcs/class/Mono.Http/Mono.Http.Modules/BasicAuthenticationModule.cs [new file with mode: 0644]
mcs/class/Mono.Http/Mono.Http.Modules/ChangeLog
mcs/class/Mono.Http/Mono.Http.Modules/DigestAuthenticationModule.cs [new file with mode: 0644]
mcs/class/Mono.Http/Mono.Http.dll.sources
mcs/class/Mono.Http/samples/ChangeLog
mcs/class/Mono.Http/samples/auth.xml [new file with mode: 0644]

diff --git a/mcs/class/Mono.Http/Mono.Http.Modules/AuthenticationModule.cs b/mcs/class/Mono.Http/Mono.Http.Modules/AuthenticationModule.cs
new file mode 100644 (file)
index 0000000..de27319
--- /dev/null
@@ -0,0 +1,97 @@
+//
+// Abstract Authentication implementation
+//
+// Authors:
+//     Greg Reinacker (gregr@rassoc.com)
+//     Sebastien Pouliot (spouliot@motus.com)
+//
+// Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
+// Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
+//
+// Based on "DigestAuthenticationModule.cs". Original source code available at
+// http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
+//
+
+using System;
+using System.Configuration;
+using System.Web;
+
+namespace Mono.Http.Modules
+{
+       abstract public class AuthenticationModule : IHttpModule
+       {
+               string authMethod;
+
+               public AuthenticationModule (string authenticationMethod) 
+               {
+                       authMethod = authenticationMethod;
+               }
+
+               public string AuthenticationMethod { 
+                       get { return authMethod; }
+               }
+
+               #region IHttpModule Members
+
+               public virtual void Init (HttpApplication context) 
+               {
+                       context.AuthenticateRequest += new EventHandler (this.OnAuthenticateRequest);
+                       context.EndRequest += new EventHandler (this.OnEndRequest);
+               }
+
+               public virtual void Dispose () {}
+
+               #endregion
+
+               #region Event Handlers
+
+               public virtual void OnAuthenticateRequest (object source, EventArgs eventArgs) 
+               {
+                       if (!AuthenticationRequired)
+                               return;
+
+                       HttpApplication app = (HttpApplication) source;
+                       string authdata = Authorization (app, AuthenticationMethod);
+                       if ((authdata == null) || (!AcceptCredentials (app, authdata))) {
+                               DenyAccess (app);
+                               return;
+                       }
+               }
+
+               abstract public void OnEndRequest (object source, EventArgs eventArgs);
+
+               #endregion
+
+               abstract protected bool AcceptCredentials (HttpApplication app, string authentication);
+
+               protected bool AuthenticationRequired {
+                       get { return (AuthenticationMethod == ConfigurationSettings.AppSettings ["Authentication"]); }
+               }
+
+               protected void DenyAccess (HttpApplication app) 
+               {
+                       app.Response.StatusCode = 401;
+                       app.Response.StatusDescription = "Access Denied";
+                       // Write to response stream as well, to give user visual 
+                       // indication of error during development
+                       app.Response.Write ("401 Access Denied");
+                       app.CompleteRequest ();
+               }
+
+               protected string Authorization (HttpApplication app, string authenticationMethod) 
+               {
+                       string autz = app.Request.Headers ["Authorization"];
+                       if ((autz == null) || (autz.Length == 0)) {
+                               // No credentials; anonymous request
+                               return null;
+                       }
+                       
+                       if (autz.ToUpper ().StartsWith (authenticationMethod.ToUpper ())) {
+                               return autz.Substring (authenticationMethod.Length + 1);
+                       }
+
+                       return null;
+               }
+       }
+}
+
diff --git a/mcs/class/Mono.Http/Mono.Http.Modules/BasicAuthenticationModule.cs b/mcs/class/Mono.Http/Mono.Http.Modules/BasicAuthenticationModule.cs
new file mode 100644 (file)
index 0000000..57b454e
--- /dev/null
@@ -0,0 +1,87 @@
+//
+// Basic Authentication implementation
+//
+// Authors:
+//     Greg Reinacker (gregr@rassoc.com)
+//     Sebastien Pouliot (spouliot@motus.com)
+//
+// Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
+// Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
+//
+// Based on "DigestAuthenticationModule.cs". Original source code available at
+// http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
+//
+
+using System;
+using System.Configuration;
+using System.IO;
+using System.Security.Principal;
+using System.Text;
+using System.Web;
+using System.Xml;
+
+namespace Mono.Http.Modules
+{
+       public class BasicAuthenticationModule : AuthenticationModule
+       {
+               static char[] separator = {':'};
+
+               public BasicAuthenticationModule () : base ("Basic") {}
+
+               protected override bool AcceptCredentials (HttpApplication app, string authentication) 
+               {
+                       byte[] userpass = Convert.FromBase64String (authentication);
+                       string[] up = Encoding.UTF8.GetString (userpass).Split (separator);
+                       string username = up [0];
+                       string password = up [1];
+
+                       string userFileName = app.Request.MapPath (ConfigurationSettings.AppSettings ["Basic.Users"]);
+                       if (userFileName == null || !File.Exists (userFileName))
+                               return false;
+
+                       XmlDocument userDoc = new XmlDocument ();
+                       userDoc.Load (userFileName);
+
+                       string xPath = String.Format ("/users/user[@name='{0}']", username);
+                       XmlNode user = userDoc.SelectSingleNode (xPath);
+
+                       if (user == null)
+                               return false;
+
+                       XmlAttribute att = user.Attributes ["password"];
+                       if (att == null || password != att.Value)
+                               return false;
+
+                       XmlNodeList roleNodes = user.SelectNodes ("role");
+                       string[] roles = new string [roleNodes.Count];
+                       int i = 0;
+                       foreach (XmlNode xn in roleNodes) {
+                               XmlAttribute rolename = xn.Attributes ["name"];
+                               if (rolename == null)
+                                       continue;
+
+                               roles [i++] = rolename.Value;
+                       }
+                       app.Context.User = new GenericPrincipal (new GenericIdentity (username, AuthenticationMethod), roles);
+                       return true;
+               }
+
+               #region Event Handlers
+
+               // We add the WWW-Authenticate header here, so if an authorization 
+               // fails elsewhere than in this module, we can still request authentication 
+               // from the client.
+               public override void OnEndRequest (object source, EventArgs eventArgs)
+               {
+                       HttpApplication app = (HttpApplication) source;
+                       if (app.Response.StatusCode != 401 || !AuthenticationRequired)
+                               return;
+
+                       string realm = ConfigurationSettings.AppSettings ["Basic.Realm"];
+                       string challenge = String.Format ("{0} realm=\"{1}\"", AuthenticationMethod, realm);
+                       app.Response.AppendHeader ("WWW-Authenticate", challenge);
+               }
+
+               #endregion
+       }
+}
index 1b1572c96ea10fceb68ec71c9894b962f292962b..23bfac28fda802ab29ccfe5b098af403da10c398 100644 (file)
@@ -1,3 +1,12 @@
+2003-12-15  Gonzalo Paniagua Javier <gonzalo@ximian.com>
+
+       * AuthenticationModule.cs:
+       * BasicAuthenticationModule.cs:
+       * DigestAuthenticationModule.cs: new modules to do Basic and Digest
+       authentication.
+
+       * samples/auth.xml: sample user/password file for the modules.
+
 2003-12-12  Gonzalo Paniagua Javier <gonzalo@ximian.com>
 
        * AcceptEncodingModule.cs: moved this file here.
diff --git a/mcs/class/Mono.Http/Mono.Http.Modules/DigestAuthenticationModule.cs b/mcs/class/Mono.Http/Mono.Http.Modules/DigestAuthenticationModule.cs
new file mode 100644 (file)
index 0000000..82c4bae
--- /dev/null
@@ -0,0 +1,208 @@
+//
+// Digest Authentication implementation
+//
+// Authors:
+//     Greg Reinacker (gregr@rassoc.com)
+//     Sebastien Pouliot (spouliot@motus.com)
+//
+// Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
+// Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
+//
+// Original source code available at
+// http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
+//
+
+using System;
+using System.Collections.Specialized;
+using System.Configuration;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Principal;
+using System.Text;
+using System.Web;
+using System.Xml;
+
+namespace Mono.Http.Modules
+{
+       public class DigestAuthenticationModule : AuthenticationModule
+       {
+               // TODO: Digest.Nonce.Lifetime="0"      Never expires
+               static int nonceLifetime = 60;
+               static char[] trim = {'='};
+
+               public DigestAuthenticationModule () : base ("Digest") {}
+
+               protected virtual bool IsValidNonce (string nonce) 
+               {
+                       DateTime expireTime;
+
+                       // pad nonce on the right with '=' until length is a multiple of 4
+                       int numPadChars = nonce.Length % 4;
+                       if (numPadChars > 0)
+                               numPadChars = 4 - numPadChars;
+                       string newNonce = nonce.PadRight(nonce.Length + numPadChars, '=');
+
+                       try {
+                               byte[] decodedBytes = Convert.FromBase64String(newNonce);
+                               string expireStr = new ASCIIEncoding().GetString(decodedBytes);
+                               expireTime = DateTime.Parse(expireStr);
+                       }
+                       catch (FormatException e) {
+                               return false;
+                       }
+
+                       return (DateTime.Now <= expireTime);
+               }
+
+               protected override bool AcceptCredentials (HttpApplication app, string authentication) 
+               {
+                       // digest
+                       ListDictionary reqInfo = new ListDictionary ();
+
+                       string[] elems = authentication.Split( new char[] {','});
+                       foreach (string elem in elems) {
+                               // form key="value"
+                               string[] parts = elem.Split (new char[] {'='}, 2);
+                               string key = parts [0].Trim (new char[] {' ','\"'});
+                               string val = parts [1].Trim (new char[] {' ','\"'});
+                               reqInfo.Add (key,val);
+                       }
+
+                       string username = (string) reqInfo ["username"];
+
+                       string userFileName = app.Request.MapPath (ConfigurationSettings.AppSettings ["Digest.Users"]);
+                       if (userFileName == null || !File.Exists (userFileName))
+                               return false;
+
+                       XmlDocument userDoc = new XmlDocument ();
+                       userDoc.Load (userFileName);
+
+                       string xPath = String.Format ("/users/user[@name='{0}']", username);
+                       XmlNode user = userDoc.SelectSingleNode (xPath);
+
+                       if (user == null)
+                               return false;
+
+                       string password = user.Attributes ["password"].Value;
+                       string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
+
+                       // calculate the Digest hashes
+
+                       // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+                       string A1 = String.Format ("{0}:{1}:{2}", username, realm, password);
+
+                       // H(A1) = MD5(A1)
+                       string HA1 = GetMD5HashBinHex (A1);
+
+                       // A2 = Method ":" digest-uri-value
+                       string A2 = String.Format ("{0}:{1}", app.Request.HttpMethod, (string)reqInfo["uri"]);
+
+                       // H(A2)
+                       string HA2 = GetMD5HashBinHex(A2);
+
+                       // KD(secret, data) = H(concat(secret, ":", data))
+                       // if qop == auth:
+                       // request-digest  = <"> < KD ( H(A1),     unq(nonce-value)
+                       //                              ":" nc-value
+                       //                              ":" unq(cnonce-value)
+                       //                              ":" unq(qop-value)
+                       //                              ":" H(A2)
+                       //                            ) <">
+                       // if qop is missing,
+                       // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
+
+                       string unhashedDigest;
+                       if (reqInfo["qop"] != null) {
+                               unhashedDigest = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
+                                       HA1,
+                                       (string)reqInfo["nonce"],
+                                       (string)reqInfo["nc"],
+                                       (string)reqInfo["cnonce"],
+                                       (string)reqInfo["qop"],
+                                       HA2);
+                       }
+                       else {
+                               unhashedDigest = String.Format("{0}:{1}:{2}",
+                                       HA1,
+                                       (string)reqInfo["nonce"],
+                                       HA2);
+                       }
+
+                       string hashedDigest = GetMD5HashBinHex (unhashedDigest);
+
+                       bool isNonceStale = !IsValidNonce((string)reqInfo["nonce"]);
+                       app.Context.Items["staleNonce"] = isNonceStale;
+
+                       bool result = (((string)reqInfo["response"] == hashedDigest) && (!isNonceStale));
+                       if (result) {
+                               XmlNodeList roleNodes = user.SelectNodes ("role");
+                               string[] roles = new string [roleNodes.Count];
+                               int i = 0;
+                               foreach (XmlNode xn in roleNodes)
+                                       roles [i++] = xn.Attributes ["name"].Value;
+
+                               IIdentity id = new GenericIdentity (username, AuthenticationMethod);
+                               app.Context.User = new GenericPrincipal (id, roles);
+                       }
+                       return result;
+               }
+
+               #region Event Handlers
+
+               public override void OnEndRequest(object source, EventArgs eventArgs)
+               {
+                       // We add the WWW-Authenticate header here, so if an authorization 
+                       // fails elsewhere than in this module, we can still request authentication 
+                       // from the client.
+
+                       HttpApplication app = (HttpApplication) source;
+                       if (app.Response.StatusCode != 401 || !AuthenticationRequired)
+                               return;
+                               
+                       string realm = ConfigurationSettings.AppSettings ["Digest.Realm"];
+                       string nonce = GetCurrentNonce ();
+                       bool isNonceStale = false;
+                       object staleObj = app.Context.Items ["staleNonce"];
+                       if (staleObj != null)
+                               isNonceStale = (bool)staleObj;
+
+                       StringBuilder challenge = new StringBuilder ("Digest realm=\"");
+                       challenge.Append(realm);
+                       challenge.Append("\"");
+                       challenge.Append(", nonce=\"");
+                       challenge.Append(nonce);
+                       challenge.Append("\"");
+                       challenge.Append(", opaque=\"0000000000000000\"");
+                       challenge.Append(", stale=");
+                       challenge.Append(isNonceStale ? "true" : "false");
+                       challenge.Append(", algorithm=MD5");
+                       challenge.Append(", qop=\"auth\"");
+
+                       app.Response.AppendHeader("WWW-Authenticate", challenge.ToString());
+                       app.Response.StatusCode = 401;
+               }
+
+               #endregion
+
+               private string GetMD5HashBinHex (string toBeHashed)
+               {
+                       MD5 hash = MD5.Create ();
+                       byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
+
+                       StringBuilder sb = new StringBuilder ();
+                       foreach (byte b in result)
+                               sb.Append (b.ToString ("x2"));
+                       return sb.ToString ();
+               }
+
+               protected virtual string GetCurrentNonce ()
+               {
+                       DateTime nonceTime = DateTime.Now.AddSeconds (nonceLifetime);
+                       byte[] expireBytes = Encoding.ASCII.GetBytes (nonceTime.ToString ("G"));
+                       string nonce = Convert.ToBase64String (expireBytes);
+                       // nonce can't end in '=', so trim them from the end
+                       nonce = nonce.TrimEnd (trim);
+                       return nonce;
+               }
+       }
+}
index 6bdc8e182cf816d3347a0f6ed4ac209b1c69313e..9ab4f6e492f3d89b01c65fd7d228a91618f247ec 100644 (file)
@@ -7,3 +7,6 @@ Mono.Http/NtlmClient.cs
 Mono.Http.Configuration/AcceptEncodingConfig.cs
 Mono.Http.Configuration/AcceptEncodingSectionHandler.cs
 Mono.Http.Modules/AcceptEncodingModule.cs
+Mono.Http.Modules/AuthenticationModule.cs
+Mono.Http.Modules/BasicAuthenticationModule.cs
+Mono.Http.Modules/DigestAuthenticationModule.cs
index c067ee7c03692003a3fed0b18ba49c9e1ab90e1c..ef3d655deed3020fd34f8c9750083e811ebddbe7 100644 (file)
@@ -1,3 +1,7 @@
+2003-12-15  Gonzalo Paniagua Javier <gonzalo@ximian.com>
+
+       * auth.xml: sample user/password file for the modules.
+
 2003-12-11  Gonzalo Paniagua Javier <gonzalo@ximian.com>
 
        * http-get-gzip.cs:
diff --git a/mcs/class/Mono.Http/samples/auth.xml b/mcs/class/Mono.Http/samples/auth.xml
new file mode 100644 (file)
index 0000000..d17b330
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This file can be used together with authentication modules -->
+<users>
+<user name="gonzalo" password="lalala">
+       <role name="user" />
+</user>
+</users>