How can I get an unsuccessful login message for unauthorized users?

mrcoulson
mrcoulson used Ask the Experts™
on
I'm trying to write an application that connects to Active Directory and only allows ONE user to authenticate.  The purpose is so I can limit some apps to only one user but still allow that user to have the same username and password as his email and computer login.

See the attached Web.config.  It works almost perfectly.  If I enter the proper password for "jeremy", I get in.  If I enter the wrong password for "jeremy", I get the unsuccessful login message.  If I enter another use, say "karen" and the wrong password, I get the unsuccessful login message.  BUT if I enter "karen" and the correct password, I get the returned to the login page WITHOUT the message.  

I know that might seem nitpicky, but I need to get the message to display because this is how I think I'll be changing a lot of our intranet apps to work.

What do I need to do to get the unsuccessful login message on login attempts with the correct password for an unauthorized user?
<?xml version="1.0"?>

<configuration>


    <configSections>
      <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
        <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
          <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
          <sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
            <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="Everywhere" />
            <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication" />
            <section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication" />
            <section name="roleService" type="System.Web.Configuration.ScriptingRoleServiceSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication" />
          </sectionGroup>
        </sectionGroup>
      </sectionGroup>
    </configSections>  

  
    <appSettings/>
  <connectionStrings>
    <add name="ADConnectionString" connectionString="LDAP://co.frederick.va.us/DC=co,DC=frederick,DC=va,DC=us" />
  </connectionStrings>
  
    <system.web>
        <!-- 
            Set compilation debug="true" to insert debugging 
            symbols into the compiled page. Because this 
            affects performance, set this value to true only 
            during development.
        -->
        <compilation debug="false">

          <assemblies>
            <add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
            <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
            <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
            <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
          </assemblies>

        </compilation>
        <!--
            The <authentication> section enables configuration 
            of the security authentication mode used by 
            ASP.NET to identify an incoming user. 
        -->
      <authentication mode="Forms">
        <forms
            name=".ADAuthCookie"
            timeout="10" loginUrl="Login.aspx">
        </forms>
      </authentication>
      <authorization>
        <deny users="?" />
        <allow users="jeremy" />
        <deny users="*" />
      </authorization>
      <membership defaultProvider="ADMembershipProvider">
        <providers>
          <clear />
          <add
             name="ADMembershipProvider"
             type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0, 
             Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             connectionStringName="ADConnectionString"
             connectionUsername="administratorusername"
             connectionPassword="administratorpassword"
             applicationName="CancellationsManager"
             attributeMapUsername="sAMAccountName"
             />
        </providers>
      </membership>
      <customErrors mode="Off"/>


      <pages>
        <controls>
          <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
          <add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        </controls>
      </pages>

      <httpHandlers>
        <remove verb="*" path="*.asmx"/>
        <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>
      </httpHandlers>
      <httpModules>
        <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </httpModules>

    </system.web>

    <system.codedom>
      <compilers>
        <compiler language="c#;cs;csharp" extension=".cs" warningLevel="4"
                  type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <providerOption name="CompilerVersion" value="v3.5"/>
          <providerOption name="WarnAsError" value="false"/>
        </compiler>
     </compilers>
    </system.codedom>
    
    <!-- 
        The system.webServer section is required for running ASP.NET AJAX under Internet
        Information Services 7.0.  It is not necessary for previous version of IIS.
    -->
    <system.webServer>
      <validation validateIntegratedModeConfiguration="false"/>
      <modules>
        <remove name="ScriptModule" />
        <add name="ScriptModule" preCondition="managedHandler" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </modules>
      <handlers>
        <remove name="WebServiceHandlerFactory-Integrated"/>
        <remove name="ScriptHandlerFactory" />
        <remove name="ScriptHandlerFactoryAppServices" />
        <remove name="ScriptResource" />
        <add name="ScriptHandlerFactory" verb="*" path="*.asmx" preCondition="integratedMode"
             type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd" preCondition="integratedMode"
             type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        <add name="ScriptResource" preCondition="integratedMode" verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </handlers>
    </system.webServer>

    <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
          <assemblyIdentity name="System.Web.Extensions" publicKeyToken="31bf3856ad364e35"/>
          <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
        </dependentAssembly>
        <dependentAssembly>
          <assemblyIdentity name="System.Web.Extensions.Design" publicKeyToken="31bf3856ad364e35"/>
          <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
        </dependentAssembly>
      </assemblyBinding>
    </runtime>

</configuration>

Open in new window

Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
Craig WagnerSoftware Architect

Commented:
Strictly speaking, you don't need the deny users="?". That is denying access to the anonymous user, but deny users="*" denies access to all users, which would include the anonymous user. That shouldn't be causing the problem you're experiencing, but give it a shot.

      <authorization>
        <allow users="jeremy" />
        <deny users="*" />
      </authorization>

Author

Commented:
I'm not in a position to try the solution right now, but I've often wondered how those work. So, each permission above takes precedence? "All users are disallowed except for Jeremy because his permission is listed first." Does it always fall top-to-bottom or is there a case when permissions declared later take precedence?
Craig WagnerSoftware Architect

Commented:
I've never had to do selective permissions like you are doing (usually I just want to deny all anonymous users), but from what I recall reading in the documentation it's like a switch statement, conditions are checked from top to bottom until a match is found. So until the user is authenticated they fall into the "all users" group and are denied. Once the user is authenticated, if they are jeremy they are allowed access, otherwise they are denied. At least that's how it should work.

Another thing that occurred to me is that you might have to turn impersonation on.

<identity impersonate="true" />

Microsoft Azure 2017

Azure has a changed a lot since it was originally introduce by adding new services and features. Do you know everything you need to about Azure? This course will teach you about the Azure App Service, monitoring and application insights, DevOps, and Team Services.

Craig WagnerSoftware Architect

Commented:
I got curious about this and built a prototype. I think you might be S.O.L. if you're looking for an simple way to do this.

The problem is that the ValidateUser method of the Membership object isn't the one that's rejecting the user's credentials. In fact, as far as that method is concerned it's got a perfectly valid user when the username/password are correct. So assuming you've got some kind of Response.Redirect (or if you're using the Login control it's doing the redirect), the system happily sends the user off to a different page. When loading the other page the Forms Auth subsystem kicks in:

"Does this user have an auth ticket?"

Yes.

"Is this user authorized?"

No.

Punt them to the login page.

So a whole bunch of back-and-forth has happened before the login page repaints. At that point the login page has no idea that the user is there as a result of being denied due to the authorization section of the web.config.

I've looked at the various methods of the FormsAuthentication class and can't find one that would let you check if the user is in a denied group.

So where does that leave you? If you've coded your own login logic (using the Membership.ValidateUser() method), you could check the results of that method call. If it returns true you could then query the web.config (using the ConfigurationManager class) to see if the user will be allowed or not. I've included the prototype code (at least as far as I got with it) to give you a start.
if( Membership.ValidateUser( txtUserName.Text, txtPassword.Text ) )
{
    AuthorizationSection authSection = 
        ConfigurationManager.GetSection( "system.web/authorization" ) as AuthorizationSection;

    if( authSection != null )
    {
        foreach( AuthorizationRule rule in authSection.Rules )
        {
            // this is the yucky part, you'll need to
            // figure out if the user fails a rule and 
            // generate an "access denied" message
        }
    }

    // if user doesn't fail an authorization rule, redirect them
    FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, false );
}
else
{
    lblFailure.Text = "Your logon failed.";
    lblFailure.Visible = true;
}

Open in new window

Craig WagnerSoftware Architect

Commented:
P.S. I found that when iterating through the rules you'll always get a rule at the end that allows everyone. I suspect that's coming from the machine.config. Unfortunately the <authorization> section doesn't seem to support the <clear /> command/tag, so you'll have to be aware that there will be one bogus rule at the end that would allow the user. You're really looking for the first rule that would cause the user to be denied access (remember, they are processed from top to bottom).

Author

Commented:
Wow, that's a ton of insight.  I'm going to put your code to the test this afternoon.  Unfortunately, I have to go play PC tech and help a coworker kill a virus or two at a remote office this morning.  As soon as I'm back, though...!

Jeremy

Author

Commented:
What if I try something like the attached (some of which came from MSDN!)? I had the idea: what if I check the username in code-behind.  Basically, I want to say:

If the username is not jeremy, the authentication fails. If it does equal jeremy carry on with checking jeremy's password.

I suppose what I have there actually says "if it's jeremy, it's successful" because I get in even when I enter a bad password.

So, can I put logic in that tests a condition and either stops or permits the login control to do its thing?

Jeremy
private bool SiteSpecificAuthenticationMethod(string UserName, string Password)
        {
            if (UserName != "jeremy")
            {
                return false;
            }
            else
            {
                return true;
            }
        }


        protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
        {
            bool Authenticated = false;
            Authenticated = SiteSpecificAuthenticationMethod(Login1.UserName, Login1.Password);
            e.Authenticated = Authenticated;
        }

Open in new window

Software Architect
Commented:
I'm not a big fan of hard-coding things into the application like that. If you ever want to allow a second user, or need to replace jeremy with someone else, you have to change the application code, recompile, and redeploy the application.

However, if you did want to go that route, just call Membership.ValidateUser(). It's in the code sample I posted earlier.

Author

Commented:
Membership.ValidateUser()

That's first on my list this morning after a news update.

I know it's not ideal, but it won't be terrible for the apps I'm doing here.  They're mostly just a Login.aspx, a Default.aspx, and their respective .cs files.  Changing the user in there is the same amount of work as changing it in the Web.config.

Time may require that it is a case of "good enough for government work". And, hey, I work for government! :)

Jeremy

Author

Commented:
Okay, here's what I'm going to do.  I'll write my own login logic and not use the login control.  My code is attached.  Of course, I won't actually alert the user about which name should be used, but I just needed to make sure I was getting the right results based on my input.

Thanks for the input.  I wouldn't have gotten there without you.  Points are forthcoming.

Jeremy
protected void btnLogin_Click(object sender, EventArgs e)
        {
            if (txtUserName.Text == "jeremy")
            {
                if (Membership.ValidateUser(txtUserName.Text, txtPassword.Text))
                {
                    FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, false);
                }
                else
                {
                    Response.Write("Login failed. Please check your user name and password and try again.");
                }
            }
            else
            {
                Response.Write("You're not Jeremy!");
            }
        }

Open in new window

Author

Commented:
Thanks a ton!  I know it's not the best solution, but it's the best I can get my brain around today.  One day when I have more time, I'll revisit your comments and try to do this the RIGHT way.

Jeremy

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial