Last active
December 20, 2017 11:08
-
-
Save ginomessmer/dca57bb9b8e688ca61ca382e5019c66a to your computer and use it in GitHub Desktop.
Basic Authentication Module for IIS (http://blog.fermium.io/nuget-server-with-basic-authentication/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Configuration; | |
| using System.Linq; | |
| using System.Net; | |
| using System.Text; | |
| using System.Text.RegularExpressions; | |
| using System.Web; | |
| // Taken from https://raw.githubusercontent.com/devbridge/AzurePowerTools/ | |
| namespace BasicAuthenticationApp.Modules | |
| { | |
| /// <summary> | |
| /// This module performs basic authentication. | |
| /// For details on basic authentication see RFC 2617. | |
| /// Based on the work by Mike Volodarsky (www.iis.net/learn/develop/runtime-extensibility/developing-a-module-using-net) | |
| /// | |
| /// The basic operational flow is: | |
| /// | |
| /// On AuthenticateRequest: | |
| /// extract the basic authentication credentials | |
| /// verify the credentials | |
| /// if succesfull, create and send authentication cookie | |
| /// | |
| /// On SendResponseHeaders: | |
| /// if there is no authentication cookie in request, clear response, add unauthorized status code (401) and | |
| /// add the basic authentication challenge to trigger basic authentication. | |
| /// </summary> | |
| public class BasicAuthenticationModule : IHttpModule | |
| { | |
| /// <summary> | |
| /// HTTP1.1 Authorization header | |
| /// </summary> | |
| public const string HttpAuthorizationHeader = "Authorization"; | |
| /// <summary> | |
| /// HTTP1.1 Basic Challenge Scheme Name | |
| /// </summary> | |
| public const string HttpBasicSchemeName = "Basic"; // | |
| /// <summary> | |
| /// HTTP1.1 Credential username and password separator | |
| /// </summary> | |
| public const char HttpCredentialSeparator = ':'; | |
| /// <summary> | |
| /// HTTP1.1 Not authorized response status code | |
| /// </summary> | |
| public const int HttpNotAuthorizedStatusCode = 401; | |
| /// <summary> | |
| /// HTTP1.1 Basic Challenge Scheme Name | |
| /// </summary> | |
| public const string HttpWwwAuthenticateHeader = "WWW-Authenticate"; | |
| /// <summary> | |
| /// The name of cookie that is sent to client | |
| /// </summary> | |
| public const string AuthenticationCookieName = "BasicAuthentication"; | |
| /// <summary> | |
| /// HTTP.1.1 Basic Challenge Realm | |
| /// </summary> | |
| public const string Realm = "demo"; | |
| /// <summary> | |
| /// The credentials that are allowed to access the site. | |
| /// </summary> | |
| private IDictionary<string, string> activeUsers; | |
| /// <summary> | |
| /// Exclude configuration - request URL is matched to dictionary key and request method is matched to the value of the same key-value pair. | |
| /// </summary> | |
| private IDictionary<Regex, Regex> excludes; | |
| /// <summary> | |
| /// Indicates whether redirects are allowed without authentication. | |
| /// </summary> | |
| private bool allowRedirects; | |
| /// <summary> | |
| /// Indicates whether local requests are allowed without authentication. | |
| /// </summary> | |
| private bool allowLocal; | |
| /// <summary> | |
| /// Regular expression that matches any given string. | |
| /// </summary> | |
| private readonly static Regex AllowAnyRegex = new Regex(".*", RegexOptions.Compiled); | |
| /// <summary> | |
| /// Dictionary that caches whether basic authentication challenge should be sent. Key is request URL + request method, value indicates whether | |
| /// challenge should be sent. | |
| /// </summary> | |
| private static IDictionary<string, bool> shouldChallengeCache = new Dictionary<string, bool>(); | |
| public void AuthenticateUser(Object source, EventArgs e) | |
| { | |
| var context = ((HttpApplication)source).Context; | |
| string authorizationHeader = context.Request.Headers[HttpAuthorizationHeader]; | |
| // Extract the basic authentication credentials from the request | |
| string userName = null; | |
| string password = null; | |
| if (!this.ExtractBasicCredentials(authorizationHeader, ref userName, ref password)) | |
| { | |
| return; | |
| } | |
| // Validate the user credentials | |
| if (!this.ValidateCredentials(userName, password)) | |
| { | |
| return; | |
| } | |
| // check whether cookie is set and send it to client if needed | |
| var authCookie = context.Request.Cookies.Get(AuthenticationCookieName); | |
| if (authCookie == null) | |
| { | |
| authCookie = new HttpCookie(AuthenticationCookieName, "1") { Expires = DateTime.Now.AddHours(1) }; | |
| context.Response.Cookies.Add(authCookie); | |
| } | |
| } | |
| public void IssueAuthenticationChallenge(Object source, EventArgs e) | |
| { | |
| var context = ((HttpApplication)source).Context; | |
| if (allowLocal && context.Request.IsLocal) | |
| { | |
| return; | |
| } | |
| if (allowRedirects && IsRedirect(context.Response.StatusCode)) | |
| { | |
| return; | |
| } | |
| if (ShouldChallenge(context)) | |
| { | |
| // if authentication cookie is not set issue a basic challenge | |
| var authCookie = context.Request.Cookies.Get(AuthenticationCookieName); | |
| if (authCookie == null) | |
| { | |
| //make sure that user is not authencated yet | |
| if (!context.Response.Cookies.AllKeys.Contains(AuthenticationCookieName)) | |
| { | |
| context.Response.Clear(); | |
| context.Response.StatusCode = HttpNotAuthorizedStatusCode; | |
| context.Response.AddHeader(HttpWwwAuthenticateHeader, "Basic realm =\"" + Realm + "\""); | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Returns true if authentication challenge should be sent to client based on configured exclude rules | |
| /// </summary> | |
| private bool ShouldChallenge(HttpContext context) | |
| { | |
| // first check cache | |
| var key = string.Concat(context.Request.Path, context.Request.HttpMethod); | |
| if (shouldChallengeCache.ContainsKey(key)) | |
| { | |
| return shouldChallengeCache[key]; | |
| } | |
| // if value is not found in cache check exclude rules | |
| foreach (var urlVerbRegex in this.excludes) | |
| { | |
| if (urlVerbRegex.Key.IsMatch(context.Request.Path) && urlVerbRegex.Value.IsMatch(context.Request.HttpMethod)) | |
| { | |
| shouldChallengeCache[key] = false; | |
| return false; | |
| } | |
| } | |
| shouldChallengeCache[key] = true; | |
| return true; | |
| } | |
| private static bool IsRedirect(int httpStatusCode) | |
| { | |
| return new[] | |
| { | |
| HttpStatusCode.MovedPermanently, | |
| HttpStatusCode.Redirect, | |
| HttpStatusCode.TemporaryRedirect | |
| }.Any(c => (int)c == httpStatusCode); | |
| } | |
| protected virtual bool ValidateCredentials(string userName, string password) | |
| { | |
| if (activeUsers.ContainsKey(userName) && activeUsers[userName] == password) | |
| { | |
| return true; | |
| } | |
| return false; | |
| } | |
| protected virtual bool ExtractBasicCredentials(string authorizationHeader, ref string username, ref string password) | |
| { | |
| if (string.IsNullOrEmpty(authorizationHeader)) | |
| { | |
| return false; | |
| } | |
| string verifiedAuthorizationHeader = authorizationHeader.Trim(); | |
| if (verifiedAuthorizationHeader.IndexOf(HttpBasicSchemeName, StringComparison.InvariantCultureIgnoreCase) != 0) | |
| { | |
| return false; | |
| } | |
| // get the credential payload | |
| verifiedAuthorizationHeader = verifiedAuthorizationHeader.Substring(HttpBasicSchemeName.Length, verifiedAuthorizationHeader.Length - HttpBasicSchemeName.Length).Trim(); | |
| // decode the base 64 encoded credential payload | |
| byte[] credentialBase64DecodedArray = Convert.FromBase64String(verifiedAuthorizationHeader); | |
| string decodedAuthorizationHeader = Encoding.UTF8.GetString(credentialBase64DecodedArray, 0, credentialBase64DecodedArray.Length); | |
| // get the username, password, and realm | |
| int separatorPosition = decodedAuthorizationHeader.IndexOf(HttpCredentialSeparator); | |
| if (separatorPosition <= 0) | |
| { | |
| return false; | |
| } | |
| username = decodedAuthorizationHeader.Substring(0, separatorPosition).Trim(); | |
| password = decodedAuthorizationHeader.Substring(separatorPosition + 1, (decodedAuthorizationHeader.Length - separatorPosition - 1)).Trim(); | |
| if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) | |
| { | |
| return false; | |
| } | |
| return true; | |
| } | |
| public void Init(HttpApplication context) | |
| { | |
| var config = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~/web.config"); | |
| var basicAuth = TraverseConfigSections<BasicAuthenticationConfigurationSection>(config.RootSectionGroup); | |
| if (basicAuth == null) | |
| { | |
| System.Diagnostics.Debug.WriteLine("BasicAuthenticationModule not started - Configuration not found. Make sure that BasicAuthenticationConfigurationSection section is defined."); | |
| return; | |
| } | |
| allowRedirects = basicAuth.AllowRedirects; | |
| allowLocal = basicAuth.AllowLocal; | |
| InitCredentials(basicAuth); | |
| InitExcludes(basicAuth); | |
| // Subscribe to the authenticate event to perform the authentication. | |
| context.AuthenticateRequest += AuthenticateUser; | |
| // Subscribe to the EndRequest event to issue the authentication challenge if necessary. | |
| context.EndRequest += IssueAuthenticationChallenge; | |
| } | |
| private void InitCredentials(BasicAuthenticationConfigurationSection basicAuth) | |
| { | |
| this.activeUsers = new Dictionary<string, string>(); | |
| for (int i = 0; i < basicAuth.Credentials.Count; i++) | |
| { | |
| var credential = basicAuth.Credentials[i]; | |
| this.activeUsers.Add(credential.UserName, credential.Password); | |
| } | |
| } | |
| private void InitExcludes(BasicAuthenticationConfigurationSection basicAuth) | |
| { | |
| var excludesAsString = new Dictionary<string, string>(); | |
| this.excludes = new Dictionary<Regex, Regex>(); | |
| var allowAnyRegex = AllowAnyRegex.ToString(); | |
| for (int i = 0; i < basicAuth.Excludes.Count; i++) | |
| { | |
| var excludeUrl = basicAuth.Excludes[i].Url; | |
| var excludeVerb = basicAuth.Excludes[i].Verb; | |
| if (string.IsNullOrEmpty(excludeUrl)) | |
| { | |
| excludeUrl = allowAnyRegex; | |
| } | |
| if (string.IsNullOrEmpty(excludeVerb)) | |
| { | |
| excludeVerb = allowAnyRegex; | |
| } | |
| excludesAsString[excludeUrl] = excludeVerb; | |
| } | |
| foreach (var url in excludesAsString.Keys) | |
| { | |
| var urlRegex = url == allowAnyRegex ? AllowAnyRegex : new Regex(url, RegexOptions.Compiled | RegexOptions.IgnoreCase); | |
| var verbRegex = excludesAsString[url] == allowAnyRegex ? AllowAnyRegex : new Regex(excludesAsString[url], RegexOptions.Compiled | RegexOptions.IgnoreCase); | |
| excludes[urlRegex] = verbRegex; | |
| } | |
| } | |
| private T TraverseConfigSections<T>(ConfigurationSectionGroup group) where T : ConfigurationSection | |
| { | |
| foreach (ConfigurationSection section in group.Sections) | |
| { | |
| if (Type.GetType(section.SectionInformation.Type, false) == typeof(T)) | |
| return (T)section; | |
| } | |
| foreach (ConfigurationSectionGroup g in group.SectionGroups) | |
| { | |
| var section = this.TraverseConfigSections<T>(g); | |
| if (section != null) | |
| { | |
| return section; | |
| } | |
| } | |
| return null; | |
| } | |
| public void Dispose() | |
| { | |
| // Do nothing here | |
| } | |
| } | |
| public class BasicAuthenticationConfigurationSection : ConfigurationSection | |
| { | |
| private const string CredentialsNode = "credentials"; | |
| private const string ExcludesNode = "excludes"; | |
| /// <summary> | |
| /// Gets or sets the credentials. | |
| /// </summary> | |
| /// <value> | |
| /// The credentials. | |
| /// </value> | |
| [ConfigurationProperty(CredentialsNode, IsRequired = false)] | |
| public CredentialElementCollection Credentials | |
| { | |
| get { return (CredentialElementCollection)this[CredentialsNode]; } | |
| set { this[CredentialsNode] = value; } | |
| } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether authenticaiton module should allow redirects without issuing auth challenge. | |
| /// </summary> | |
| /// <value> | |
| /// <c>true</c> to allow redirects; otherwise, <c>false</c>. | |
| /// </value> | |
| [ConfigurationProperty("allowRedirects", DefaultValue = "false", IsRequired = false)] | |
| public bool AllowRedirects | |
| { | |
| get { return (bool)this["allowRedirects"]; } | |
| set { this["allowRedirects"] = value; } | |
| } | |
| /// <summary> | |
| /// Gets or sets a value indicating whether authenticaiton module should allow local requests without issuing auth challenge. | |
| /// </summary> | |
| /// <value> | |
| /// <c>true</c> to allow redirects; otherwise, <c>false</c>. | |
| /// </value> | |
| [ConfigurationProperty("allowLocal", DefaultValue = "false", IsRequired = false)] | |
| public bool AllowLocal | |
| { | |
| get { return (bool)this["allowLocal"]; } | |
| set { this["allowLocal"] = value; } | |
| } | |
| /// <summary> | |
| /// Gets or sets the URL exclusions. | |
| /// </summary> | |
| /// <value> | |
| /// The URL exclusions. | |
| /// </value> | |
| [ConfigurationProperty(ExcludesNode, IsRequired = false)] | |
| public ExcludeElementCollection Excludes | |
| { | |
| get { return (ExcludeElementCollection)this[ExcludesNode]; } | |
| set { this[ExcludesNode] = value; } | |
| } | |
| } | |
| [ConfigurationCollection(typeof(ExcludeElement), CollectionType = ConfigurationElementCollectionType.BasicMap)] | |
| public class ExcludeElementCollection : ConfigurationElementCollection | |
| { | |
| public ExcludeElement this[int index] | |
| { | |
| get | |
| { | |
| return (ExcludeElement)BaseGet(index); | |
| } | |
| set | |
| { | |
| if (BaseGet(index) != null) | |
| { | |
| BaseRemoveAt(index); | |
| } | |
| BaseAdd(index, value); | |
| } | |
| } | |
| protected override ConfigurationElement CreateNewElement() | |
| { | |
| return new ExcludeElement(); | |
| } | |
| protected override object GetElementKey(ConfigurationElement element) | |
| { | |
| return ((ExcludeElement)element).ToString(); | |
| } | |
| } | |
| public class ExcludeElement : ConfigurationElement | |
| { | |
| private const string UrlAttribute = "url"; | |
| private const string VerbAttribute = "verb"; | |
| /// <summary> | |
| /// Gets or sets the url to exclude. | |
| /// </summary> | |
| /// <value> | |
| /// The url. | |
| /// </value> | |
| [ConfigurationProperty(UrlAttribute, IsRequired = false, IsKey = false)] | |
| public string Url | |
| { | |
| get { return Convert.ToString(this[UrlAttribute]); } | |
| set { this[UrlAttribute] = value; } | |
| } | |
| /// <summary> | |
| /// Gets or sets the verb to exclude. | |
| /// </summary> | |
| /// <value> | |
| /// The verb. | |
| /// </value> | |
| [ConfigurationProperty(VerbAttribute, IsRequired = false, IsKey = false)] | |
| public string Verb | |
| { | |
| get { return Convert.ToString(this[VerbAttribute]); } | |
| set { this[VerbAttribute] = value; } | |
| } | |
| public override string ToString() | |
| { | |
| return string.Concat(Url, '_', Verb); | |
| } | |
| } | |
| [ConfigurationCollection(typeof(CredentialElement), CollectionType = ConfigurationElementCollectionType.BasicMap)] | |
| public class CredentialElementCollection : ConfigurationElementCollection | |
| { | |
| public CredentialElement this[int index] | |
| { | |
| get | |
| { | |
| return (CredentialElement)BaseGet(index); | |
| } | |
| set | |
| { | |
| if (BaseGet(index) != null) | |
| { | |
| BaseRemoveAt(index); | |
| } | |
| BaseAdd(index, value); | |
| } | |
| } | |
| protected override ConfigurationElement CreateNewElement() | |
| { | |
| return new CredentialElement(); | |
| } | |
| protected override object GetElementKey(ConfigurationElement element) | |
| { | |
| return ((CredentialElement)element).UserName; | |
| } | |
| } | |
| public class CredentialElement : ConfigurationElement | |
| { | |
| private const string UserNameAttribute = "username"; | |
| private const string PasswordAttribute = "password"; | |
| /// <summary> | |
| /// Gets or sets the username. | |
| /// </summary> | |
| /// <value> | |
| /// The user name. | |
| /// </value> | |
| [ConfigurationProperty(UserNameAttribute, IsRequired = true)] | |
| public string UserName | |
| { | |
| get { return Convert.ToString(this[UserNameAttribute]); } | |
| set { this[UserNameAttribute] = value; } | |
| } | |
| /// <summary> | |
| /// Gets or sets the password. | |
| /// </summary> | |
| /// <value> | |
| /// The password. | |
| /// </value> | |
| [ConfigurationProperty(PasswordAttribute, IsRequired = true)] | |
| public string Password | |
| { | |
| get { return Convert.ToString(this[PasswordAttribute]); } | |
| set { this[PasswordAttribute] = value; } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <configSections> | |
| <section name="basicAuth" type="Devbridge.BasicAuthentication.Configuration.BasicAuthenticationConfigurationSection" /> | |
| ... | |
| </configSections> | |
| <basicAuth> | |
| <credentials> | |
| <add username="username" password="password"/> | |
| </credentials> | |
| </basicAuth> | |
| <system.webServer> | |
| <validation validateIntegratedModeConfiguration="false" /> | |
| <modules runAllManagedModulesForAllRequests="true"> | |
| <add name="MyBasicAuthenticationModule" type="Devbridge.BasicAuthentication.BasicAuthenticationModule" /> | |
| ... | |
| </modules> | |
| <system.webServer> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment