Active Directory Using Form Auth and Windows Auth - c#

I have an ASP.Net Application. The requirement is to implement Form Authentication using ADFS.
If the user is accessing the WebSite from within the Domain(the same Domain as the Active Directoris), then the Form Authentication should be performed.
i.e. Using the User's Windows logged in email Id, we should check if the user exists in the Active Directory or not. If the user exists, then the Website is made accessible to the user.
If the user is not found on the basis of his/her email id, then the user is asked his/her UserName and Password, and to select one of the Two Active Directories on which the user should be searched.
(PN: There are two Active Directories. One Default for using with-in the Domain.)
If the User is accessing the Website from outside the Domain, then the user is always asked his/her UserName and Password, and to select one of the two Active Directories to which the User Belongs.
So, there is one URL to access the Website from with in the Domain, and one to access from Outside the Domain.
And I need help to accomplish this task.
The project is in Dot.Net, using Framework 3.5 on ASP.Net and C#.
Help with code solution highly appreciated.

I have done this. The basic idea is that your main form of authentication is Forms. However you make your default login page use Windows authentication. If the Windows authentication succeeds, then you create the Forms ticket and proceed. If not, then you display the login page.
The only caveat is that since Windows authentication always sends a 401 response to the browser (challenging it for Windows credentials), then non-Domain users will always get a credentials pop-up that they will have to click Cancel on.
I used MVC in my project. My Windows login page is /Login/Windows and my manual login page is /Login.
Here are the relevant areas of my web.config:
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Login/Windows" defaultUrl="~/" name=".MVCFORMSAUTH" protection="All" timeout="2880" slidingExpiration="true" />
</authentication>
<system.web>
<location path="Login">
<system.web>
<authorization>
<allow users="?" />
<allow users="*" />
</authorization>
</system.web>
</location>
<location path="Login/Windows">
<system.webServer>
<security>
<authentication>
<windowsAuthentication enabled="true" />
<anonymousAuthentication enabled="false" />
</authentication>
</security>
<httpErrors errorMode="Detailed" />
</system.webServer>
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
Here is my LoginController:
[RoutePrefix("Login")]
public class LoginController : Controller {
[Route("")]
public ActionResult Login() {
//Clear previous credentials
if (Request.IsAuthenticated) {
FormsAuthentication.SignOut();
Session.RemoveAll();
Session.Clear();
Session.Abandon();
}
return View();
}
[Route("")]
[HttpPost]
public ActionResult TryLogin(string username, string password) {
//Verify username and password however you need to
FormsAuthentication.RedirectFromLoginPage(username, true);
return null;
}
[Route("Windows")]
public ActionResult Windows() {
var principal = Thread.CurrentPrincipal;
if (principal == null || !principal.Identity.IsAuthenticated) {
//Windows authentication failed
return Redirect(Url.Action("Login", "Login") + "?" + Request.QueryString);
}
//User is validated, so let's set the authentication cookie
FormsAuthentication.RedirectFromLoginPage(principal.Identity.Name, true);
return null;
}
}
Your Login View will just be a normal username / password form that does a POST to /Login.
At this point, you have a /Login page that people can manually go to to login. You also have a /Login/Windows page that is the default login page that people are automatically redirected to. But if Windows login fails, it'll display a generic 401 error page.
The key to making this seamless is using your Login view as your custom 401 error page. I did that by highjacking the response content in Application_EndRequest using the ViewRenderer class written by Rick Strahl.
Global.asax.cs:
protected void Application_EndRequest(object sender, EventArgs e) {
if (Response.StatusCode != 401 || !Request.Url.ToString().Contains("Login/Windows")) return;
//If Windows authentication failed, inject the forms login page as the response content
Response.ClearContent();
var r = new ViewRenderer();
Response.Write(r.RenderViewToString("~/Views/Login/Login.cshtml"));
}
Another caveat I've found is that this doesn't work in IIS Express (although it's been a version or two since I last tried). I have it setup in IIS and point the debugger at that.

There is an OOTB solution that may work for you.
Use a ADFS WAP as well and set up split-brain DNS.
Internal users (inside the domain) get the DNS of the ADFS box. The default is Windows auth. (IWA)
External users (outside the domain) get the DNS of the ADFS WAP box. The default is FBA.

Related

Best way to redirect user to Login page on cache lost

What is the best way to redirect user to login when cache is lost?
At moment I'm doing this:
if (Session["Id"] == null)
{
return RedirectToAction("Login", "Home");
}
But with this method, it put this code in every function, is there any other way to do this in the entire program? I tried search by doing this from web.config, but no results.
Assuming you are using Form-based authentication and permitting access to only authorised users then this could be achieved through making changes in web.config
<authentication mode="Forms">
<forms loginUrl="login.aspx" defaultUrl="default.aspx" name=".YourApplication" timeout="60" cookieless="AutoDetect" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
the loginUrl property tells the server where to direct the user if he is not logged in, and the defaultUrl property tells the server where to direct him after the user is logged.
the deny users="?" tells the server to deny any user that is not authenticated and directs him to the loginUrl page
In the codebehind of the login page, you need something like this after you check the credentials inserted:
FormsAuthentication.RedirectFromLoginPage(userName.Text, True)
Reference
private const string GlobalAuthKey = "GlobalAuthTime";
You can insert all auth user info in authList.
HttpRuntime.Cache.Insert(GlobalAuthKey, authList);
Then Get cache :
var authList = HttpRuntime.Cache.Get(GlobalAuthKey) as List<AuthInfo> ?? new
List<AuthInfo>();
After that you check this specific user login info,and you force your user by this desire condition.Hopefully You understand.

Role-based restriction not working for ASP.NET MVC Forms Authentication via AD

I've got a ASP.NET MVC page that I'd like to secure with a login and not only authenticate against an Active Directory using Forms Authentication, but also grant access only to specific roles.
web.config
<system.web>
<authentication mode="Forms">
<forms name=".ADAuthCookie" loginUrl="~/Home/Login" timeout=45 protection="All" />
</authentication>
<authorization>
<allow roles="Admin" />
<deny users="*" />
</authorization>
...
Controllers
[HttpGet]
public ActionResult Index() {
return View("~/ng-app/index_template.cshtml");
}
[HttpGet, AllowAnonymous]
public ActionResult Login() {
return View("~/ng-app/login_template.cshtml");
}
[HttpPost, AllowAnonymous]
public ActionResult Login(LoginDto dto) {
... // validate dto & stuff
FormsAuthentication.SetAuthCookie(loginModel.Username, loginModel.RememberMe);
}
Now, basic protection and general authentication works perfectly. I can log in with my domain account and I don't have access to any other pages as anonymous users. However, I'm somehow unable to restrict access only to a certain role. When I add <allow roles="Admin" /> to the authorization section, it does absolutely nothing. When I additionally add <deny users="*" />, I lock myself out and even after successful login, the server always returns 302 Found without doing any redirects or serving the actual file.
You should be doing the allowed roles on the controller declaration
So above your actionresult declaration put this above
[HttpGet]
[Authorize(Roles="Admin")]
public ActionResult AuthorizedView() {
return View("~/ng-app/admin_template.cshtml");
}
This will then do a check to see if the user is in the role declared or not
To declare the roles in the webconfig you could do something like below
<authorizationConfiguration>
<controllerAuthorizationMappings>
<add controller="Home" role="GeneralAccess">
<!-- Define allowed roles for actions under the Home Controller -->
<actionAuthorizationMappings>
<add action="MyTopSecretActionForSuperCoolPeopleOnly" roles="Developer,Manager,Fonzie" />
</actionAuthorizationMappings>
</add>
</controllerAuthorizationMappings>
</authorizationConfiguration>
And here is a link to the site
http://www.ryanmwright.com/2010/04/25/dynamic-controlleraction-authorization-in-asp-net-mvc/
There is far to much for me to bring it into this thread, I will turn it in to a wiki answer when I have 5 minutes

Validating a user on every page using session information, c# & ASP.NET

My question is more of a request for advice on how to implement a feature to a website using session information. I have a website that asks for username and password on the landing page. When a user successfully logs in, a session is created that stores their username and a few other variables related to the account. In the Page_Load function immediately following login, I successfully access this information attached to the user via session variables, verifying that it is implemented properly.
In the MySession class:
private MySession()
{
Username = Data.User.lblUsername;
CompanyId = Data.User.lblCompanyId;
}
In my welcome page (immediately following successful login)
private void Page_Load(object sender, System.EventArgs e)
{
secure_username = MySession.Current.Username;
...
}
I want to use this information, i.e. secure_username to validate the user once a page is accessed so that people can no longer skip the login form. From MySession class, do I simply use if(MySession.Current.Username == ??) statements to check each variable at the start of the Page_Load function, or is there a proper way to go about this?
Please ask for clarification if it is needed. Thanks.
EDIT: Based on some of the responses given, i think it is important to note that if a user suddenly becomes anonymous (or times out) they should be redirected to the login page.
If you are using form authentication then you can add following tags to web.config to deny any anonymous access. If you do this, you don't have to check if user is logged in on every page.
<authorization>
<deny users="?" />
</authorization>
Add this to allow all users to see Login.aspx so that they can login
<location path="Login.aspx" allowOverride="false">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
Add this so that all your css/images folders are visible to anonymous users
<location path="css" allowOverride="false">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
<location path="images" allowOverride="false">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
You may have to add extra location tags based on your website setup and the folders you want any anonymous user to have access to
If login form authentication session expires user will be redirected to url you mention in this tag
<forms loginUrl="Login.aspx" timeout="30">
</forms>
If you need to do the same action on every page it sounds like you need a base class where all your secure pages inherit from:
Base page:
public class BasePage : System.Web.UI.Page
{
public user secure_username
{get;set;}
protected void Page_Load(object sender, EventArgs e)
{
//add your checks that repeat on each page
}
}
Other pages:
public class AuthenticatedUsersPage : BasePage
{
}
Note: This of course assumes that you have already taken care of authorization and authentication on your config file (see rs' answer for more info). Also, if you are using forms authentication, remember to make your session last longer than your forms authentication timeout.

Dual forms authentication in asp.net 2.0 - 4.0

BACKGROUND
I have two websites under the same domain. One website encapsulates Login/Account Management functions only and the other website is the real website.
LoginWeb - only Login Page/css/images/javascript folder allow anonymous access
AdminWebsite - completely locked down via forms auth. i.e not even javascript/images/css folders have <authorization><allow users=*>
WHAT I AM TRYING TO ACHIEVE
This is the workflow I am trying to achieve and have accomplished 99% of it :
If the user hits any page on AdminWebsite he is sent back to LoginWeb by FormsAuthentication
User supplies credentials in the loginpage and LoginWeb website issues a forms auth ticket. User is still not logged into AdminWebSite yet. (This way user can go to changepassword page etc and do account management functions)
User goes to two more steps in LoginWeb and now can be issued the FormsAuth ticket for AdminWebsite and redirected to it.
99% IS ALREADY WORKING
This is the code in LoginWeb which tries to issue the FormsAuth ticket for AdminWeb, but it is not working. i.e AdminWeb is still redirecting me back to Login. I am sure there is something very trivial I am missing but I can't figure out what exactly it is?????????
public void SetAuthenticationTicket(string username)
{
MachineKeySection sec=(MachineKeySection)WebConfigurationManager.OpenWebConfiguration("").SectionGroups["system.web"].Sections["machineKey"];
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
username,
DateTime.Now,
DateTime.Now.AddMinutes(30),
false, //true or false
sec.DecryptionKey, //Custom data like your webkey can go here
FormsAuthentication.FormsCookiePath);
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie("ADMINWEB", encryptedTicket)
{
Path = FormsAuthentication.FormsCookiePath,
Domain = "xxx.com"
};
Response.AppendCookie(cookie);
}
These are my web.config sections for Forms Auth:
LoginWeb
<machineKey validationKey="XXXXXX" decryptionKey="XXX" validation="SHA1"/>
<authentication mode="Forms">
<forms name="LoginWeb"
domain="xxx.com"
loginUrl="~/account/Logon"
timeout="1440"
cookieless="UseCookies"
slidingExpiration="false"
protection="All"
path="/"/>
</authentication>
AdminWebSite
<machineKey validationKey="XXXXXX" decryptionKey="XXX" validation="SHA1"/>
<authentication mode="Forms">
<forms name="ADMINWEB"
domain="xxx.com"
loginUrl="http://loginweb/account/Logon"
timeout="1440"
cookieless="UseCookies"
slidingExpiration="false"
protection="All"
path="/"/>
</authentication>
<authorization>
<deny users="?"/>
</authorization>
This site, Forms Authentication Across Applications, implies that the Name on the forms attribute should be the same.

Context.User losing Roles after being assigned in Global.asax.Application_AuthenticateRequest

I am using Forms authentication in my asp.net (3.5) application. I am also using roles to define what user can access which subdirectories of the app. Thus, the pertinent sections of my web.config file look like this:
<system.web>
<authentication mode="Forms">
<forms loginUrl="Default.aspx" path="/" protection="All" timeout="360" name="MyAppName" cookieless="UseCookies" />
</authentication>
<authorization >
<allow users="*"/>
</authorization>
</system.web>
<location path="Admin">
<system.web>
<authorization>
<allow roles="Admin"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
Based on what I have read, this should ensure that the only users able to access the Admin directory will be users who have been Authenticated and assigned the Admin role.
User authentication, saving the authentication ticket, and other related issues all work fine. If I remove the tags from the web.config file, everything works fine. The problem comes when I try to enforce that only users with the Admin role should be able to access the Admin directory.
Based on this MS KB article along with other webpages giving the same information, I have added the following code to my Global.asax file:
protected void Application_AuthenticateRequest(Object sender, EventArgs e) {
if (HttpContext.Current.User != null) {
if (Request.IsAuthenticated == true) {
// Debug#1
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(Context.Request.Cookies[FormsAuthentication.FormsCookieName].Value);
// In this case, ticket.UserData = "Admin"
string[] roles = new string[1] { ticket.UserData };
FormsIdentity id = new FormsIdentity(ticket);
Context.User = new System.Security.Principal.GenericPrincipal(id, roles);
// Debug#2
}
}
}
However, when I try to log in, I am unable to access the Admin folder (get redirected to login page).
Trying to debug the issue, if I step through a request, if I execute Context.User.IsInRole("Admin") at the line marked Debug#1 above, it returns a false. If I execute the same statement at line Debug#2, it equals true. So at least as far as Global.asax is concerned, the Role is being assigned properly.
After Global.asax, execution jumps right to the Login page (since the lack of role causes the page load in the admin folder to be rejected). However, when I execute the same statement on the first line of Page_Load of the login, it returns false. So somewhere after Application_AuthenticateRequest in Global.asax and the initial load of the WebForm in the restricted directory, the role information is being lost, causing authentication to fail (note: in Page_Load, the proper Authentication ticket is still assigned to Context.User.Id - only the role is being lost).
What am I doing wrong, and how can I get it to work properly?
Update: I entered the solution below
Here was the problem and solution:
Earlier in development I had gone to the Website menu and clicked on Asp.net configuration. This resulted in the following line being added to the web.config:
<system.web>
<roleManager enabled="true" />
</system.web>
From that point on, the app was assuming that I was doing roles through the Asp.net site manager, and not through FormsAuthentication roles. Thus the repeated failures, despite the fact that the actual authentication and roles logic was set up correctly.
After this line was removed from web.config everything worked perfectly.
this is just a random shot, but are you getting blocked because of the order of authorization for Admin? Maybe you should try switching your deny all and your all Admin.
Just in case it's getting overwritten by the deny.
(I had code samples in here but they weren't showing up.

Categories