I've created my own Membership Provider where I have below method:
public override bool ValidateUser(string username, string password)
{
if (username == "John")
return true;
else
return false;
}
I've also added below lines to web.config file:
<authentication mode="Windows" />
<authorization>
<deny users="?" />
</authorization>
<membership defaultProvider="MembershipProviter">
<providers>
<clear />
<add name="cls_MembershipProvider" type="App.cls_MembershipProvider"
enablePasswordRetrieval="false"
enablePasswordReset="false"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="5"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="App"
/>
</providers>
</membership>
As you may notice I am using Windows authentication and I don't have Log In page. By default all users from Active Directory has access to the page. My goal is to check if user exist in my database.
Everywhere I searched, there is Log In page, where ValidateUser is launched. My question is where should I implement ValidateUser method as I don't have Log In page. I just want to have control on each Controler method so I could add [Authorize] so only users from my database can actually access the page.
You can define your own CustomAuthorizeAttribute deriving from AuthorizeAttribute. Override OnAuthorization method to perform validation using details in context. Apply your custom filter on top of each controller or define a BaseController and derive your controllers from BaseController. For example you can define a class like:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class RdbiAuthorizationAttribute : AuthorizeAttribute
{
/// <summary>
/// Verifies that the logged in user is a valid organization user.
/// </summary>
/// <param name="filterContext"></param>
public override void OnAuthorization(AuthorizationContext filterContext)
{
Guard.ArgumentNotNull(filterContext, "filterContext");
Guard.ArgumentNotNull(filterContext.Controller, "filterContext.Controller");
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(
typeof(AllowAnonymousAttribute), inherit: true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(
typeof(AllowAnonymousAttribute), inherit: true);
if (skipAuthorization)
{
return;
}
if (string.IsNullOrEmpty(filterContext.HttpContext.User.Identity.Name))
throw new AuthenticationException("User must be logged in to access this page.");
var controller = filterContext.Controller as BaseController;
if (controller != null)
{
var user = controller.GetUser();
if (user == null)
{
throw new InvalidOperationException(string.Format("Logged in user {0} is not a valid user", filterContext.HttpContext.User.Identity.Name));
}
}
base.OnAuthorization(filterContext);
}
}
Then you can define controller like:
[RdbiAuthorization]
public class BaseController : Controller
{
}
public class MyTestController : BaseController
{
}
Related
I have the following problem with using MVC4 SimpleMembership: Register and Login methods work fine in local debugg mode, but after publish they crash the site. The cause from what i saw is that for example when i register an user the information is stored in my UserProfile table but nothing in webpages_Membership table. So it would appear that the WebSecurity can't access for some reason the webpages_Membership table.
I configured a basic MVC4 project to use SimpleMembership by enabling it in my WebConfig file like this:
<membership defaultProvider="DefaultMembershipProvider">
<providers>
<add name="DefaultMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" />
</providers>
</membership>
<roleManager defaultProvider="DefaultRoleProvider">
<providers>
<add name="DefaultRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData" />
</providers>
</roleManager>
Added my connectionstring:
<add name="RMBase" providerName="System.Data.SqlClient" connectionString="Data Source=SQL5003.Smarterasp.net;Initial Catalog=DB_9C0558_test;User Id=DB_9C0558_test_admin;Password=*****" />
Initialised the connection in Global.asax:
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
WebSecurityConfig.InitializeDatabase();
And in my App_Start folder i added the class for WebSecurityConfig
public static void InitializeDatabase()
{
WebSecurity.InitializeDatabaseConnection("RMBase", "UserProfile", "UserID", "UserName", true);
}
Added my DBContext class
public class RMDatabase : DbContext
{
public RMDatabase() : base("RMBase") { }
}
And my Login/Register methods look like this:
[HttpGet]
public ActionResult Login()
{
return View(new Authentication());
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(Authentication model)
{
if (!ModelState.IsValid)
{
return View(model);
}
if (WebSecurity.Login(model.Username, model.Password))
{
return RedirectToAction("Index","Home");
}
else
{
ViewBag.ErrorMessage = "incorect";
return View(model);
}
}
[HttpGet]
public ActionResult Register()
{
return View(new Registration());
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(Registration model)
{
if (ModelState.IsValid)
{
if (!WebSecurity.UserExists(model.Username))
{
WebSecurity.CreateUserAndAccount(model.Username, model.Password);
WebSecurity.Login(model.Username, model.Password);
return RedirectToAction("Index","Home");
}
ViewBag.ErrorMessage = "incorect";
}
return View(model);
}
Anyone see something that i did wrong that may be the cause of this?
Install Elmah.MVC, then in the Application_Error event in Global.asax, add
var exception = Server.GetLastError();
Elmah.ErrorSignal.FromCurrentContext().Raise(exception);
Then, on the site, navigate to /elmah, which should now have the exception logged for you.
After following Tieson T. advice i found that i had System.Web.Helpers, Version=2.0.0.0 missing from my web project referencess. Added it and all is good. Still don't understand how it was working in local debugg but i am glad that it was fixed.
I am currently using the AspNetWindowsTokenRoleProvider to provide the authorization for my controller actions:
[Authorize(Roles = "domain\\group")]
public ActionResult Index()
{
code.....
}
Rather than hard code the role name ("domain\group"), or use a constant. I would like to be to replace it with a call to a settings class which will get it from a database or file.
I figure that either there is a way to do this built into the provider or I need to replace the provider with my own implementation.
I have drawn a blank googling, so I guess I am not asking the right questions!
Could anyone please point me in the right direction to achieve this.
Thanks
I kind of worked it out, so here is the solution in case anyone wants to do the same thing.
Create a new class inheriting from WindowsTokenRoleProvider
public class MyADProvider : WindowsTokenRoleProvider
{
//settings key
public const string Users = "Authorisation.AdGRoup.Users";
public const string Admins = "Authorisation.AdGRoup.Admins";
private ISettingsRepository settingsRepository;
public override string[] GetRolesForUser(string username)
{
// settings repository reads from settings file or DB
// actual implementation is up to you
this.settingsRepository = new SettingsRepository();
// get all the AD roles the user is in
var roles = base.GetRolesForUser(username);
List<string> returnedRoles = new List<string>
{
this.GetADRole(roles, Admins),
this.GetADRole(roles, Users)
};
return returnedRoles.ToArray();
}
private string GetADRole(string[] usersAdRoles, string roleSettingName)
{
//Get the actual name of the AD group we want from the settings
var settingName = this.settingsRepository.GetSetting(roleSettingName);
return usersAdRoles.Contains(settingName) ? roleSettingName : string.Empty;
}
}
Then change the web.config to use the new class:
<roleManager enabled="true" defaultProvider="AspNetWindowsTokenRoleProvider">
<providers>
<clear />
<add name="AspNetWindowsTokenRoleProvider" type="MyADProvider" applicationName="/" />
</providers>
</roleManager>
Then I can use the settings key in the code:
[Authorize(Roles = MysADProvider.Admins)]
public ActionResult Index()
{}
I have ASP.NET MVC4 project with custom AuthorizeAttribute to control the authorization. In order to explain my situation easier I created a dummy project to illustrate the issue.
I have one single controller
using System.Web.Mvc;
using MvcApplication2.Helper;
using MvcApplication2.Models;
namespace MvcApplication2.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new ViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(ViewModel model)
{
Session["ModelValue"] = model.InputValue;
return RedirectToAction("Step2");
}
[MyAuthorize]
public ActionResult Step2()
{
return View();
}
}
}
The purpose is very simple, From Index I accept some input, store the value in a session and redirect to Step2. I have my authorize attribute on step 2. The code for my attribute is
using System;
using System.Web;
using System.Web.Mvc;
namespace MvcApplication2.Helper
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Session["ModelValue"] == null)
{
return false;
}
else
{
string inputValue = httpContext.Session["ModelValue"] as string;
if (inputValue != "1")
{
return false;
}
else
{
return true;
}
}
}
}
}
The purpose is very simple, check if the session value is 1 or not.
Run the application, you input 1 in textbox and you see step2 view, if you input anything else you get the default 401.0 page.
Now I opened the web.config file to include
<system.web>
<customErrors mode="On" defaultRedirect="~/Error">
<error statusCode="401" redirect="~/401.htm" />
</customErrors>
<compilation debug="true" targetFramework="4.0" />
</system.web>
I hope when the application captures 401 code, I should see the content of 401.htm. But the reality is that I still get the default 401 error display from server.
Any idea why?
In addition use this:
<system.webServer>
<httpErrors>
<error statusCode="401" path="~/Home/Login"/>
<error statusCode="404" path="~/Error/NotFound"/>
</httpErrors>
</system.webServer>
I have some simple DbContext:
public class AuthContext : DbContext
{
public AuthContext() : base("AuthContext")
{
}
public DbSet<User> Users { get; set; }
}
And simple User model:
[Table("User")]
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string Login { get; set; }
}
What I need is to seed data to WebSecurity always or after model creating. I have tried:
Database.SetInitializer(new AuthDbSeeder());
//--------------------------------------------------------
<add name="AuthContext" connectionString="Data Source=.\SQLEXPRESS; Initial Catalog=ChatAuth; Integrated Security=SSPI; MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
and in <system.web> I added:
<roleManager enabled="true" defaultProvider="SimpleRoleProvider">
<providers>
<clear/>
<add name="SimpleRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData"/>
</providers>
</roleManager>
<membership defaultProvider="SimpleMembershipProvider">
<providers>
<clear/>
<add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" />
</providers>
</membership>
//--------------------------------------------------------
public class AuthDbSeeder : DropCreateDatabaseAlways<AuthContext>
{
protected override void Seed(AuthContext context)
{
WebSecurity.InitializeDatabaseConnection("AuthContext", "User", "UserId", "Login",
autoCreateTables: true);
WebSecurity.CreateUserAndAccount("Sergey", "1234");
But after all I have error that Database can't be droped because it is already in use. I need some working method to seed data to WebSecurity.
Also very important for me is: how I can add my custom models in the same DbContext and seed data to this context properly.
Also any ideas how I can Unit test WebSecurity?
There is an example on how to seed and customize WebSecurity here.
I have found the solution for the seed question
I reworked SimpleMembershipInitializer:
private class SimpleMembershipInitializer
{
public SimpleMembershipInitializer()
{
try
{
WebSecurity.InitializeDatabaseConnection("AuthContext", "User", "UserId", "Login",
autoCreateTables: true);
}
catch (Exception ex)
{
throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
}
}
}
And in my seed method call
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
LazyInitializer.EnsureInitialized(ref initializer, ref isInitialized, ref initializerLock);
}
with null as the parameter.
Unit testing is still a problem
I am currently implementing a Federated Authentication solution using:
A passive STS for issuing tokens, a Website hosting a Silverlight application and WCF services for the Silverlight App.
So far I am able:
Get redirected to the STS
Login and get redirected to the Website
Display the claims on the website by accessing
HttpContext.Current.User.Identity as IClaimsIdentity;
on the web.config of the Website, I have added the two WIF modules needed (under IIS 7)
<modules runAllManagedModulesForAllRequests="true">
<add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
</modules>
I have also configured the Microsoft.IdentityModel section of the web.config to use my own implementation of ClaimsAuthenticationManager and ClaimsAthorizationManager.
<service name="Rem.Ria.PatientModule.Web.WebService.PatientService">
<claimsAuthenticationManager type ="Rem.Infrastructure.WIF.RemClaimsAuthenticationManager"/>
<claimsAuthorizationManager type ="Rem.Infrastructure.WIF.RemClaimsAuthorizationManager"/>
</service>
My ClaimsAuthenticationMAnager is simply setting the Thread.CurrentPrincipal is a valid Principal is provided.
class RemClaimsAuthenticationManager : ClaimsAuthenticationManager
{
public override IClaimsPrincipal Authenticate ( string resourceName, IClaimsPrincipal incomingPrincipal )
{
if ( incomingPrincipal.Identity.IsAuthenticated )
{
Thread.CurrentPrincipal = incomingPrincipal;
}
return incomingPrincipal;
}
}
}
The problem is that when my ClaimsAuthorizationManager is called, the context.Principal.Identity does not contain a valid Identity with Claims, and neither does the Thread.CurrentPrincipal.
Any ideas?
You don't need to set the Thread.CurrentPrincipal because the session module will do this for you. You will need to access it through the HttpContext.Current.User because the Thread.Principal is usually set on a different thread than the one accessing your service because it is two different modules in IIS. We have an example of this in our upcoming book that you can check out at our Codeplex Site.
HTH
The following sample code shows a sample class which inherits ClaimsAuthenticationManager. It just receives the incoming IClaimsPrincipal and passes through the claims, except the Name claim, which is modified. This does not set the CurrentPrincipal on the current thread, as in your example.
My test implementation is as follows:
public class CustomClaimsAuthenticationManager : ClaimsAuthenticationManager
{
public CustomClaimsAuthenticationManager()
{
}
public override IClaimsPrincipal Authenticate(string resourceName,
IClaimsPrincipal incomingPrincipal)
{
var outgoingIdentity = GetClaimsAsPassthrough(incomingPrincipal);
return outgoingIdentity;
}
private IClaimsPrincipal GetClaimsAsPassthrough(IClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return incomingPrincipal;
}
var ingoingClaims = incomingPrincipal.Identity as IClaimsIdentity;
ClaimsIdentity outgoingIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, (incomingPrincipal.Identity.Name + "
a very cool guy"))
}, incomingPrincipal.Identity.AuthenticationType);
foreach (var claim in ingoingClaims.Claims.Where(
c => c.ClaimType != ClaimTypes.Name))
{
outgoingIdentity.Claims.Add(claim.Copy());
}
return new ClaimsPrincipal(new List<ClaimsIdentity> { outgoingIdentity });
}
}