Reputation: 31
I have looked in many places and there are some samples codes here and there pertaining to WIF and WCF, consuming WCF services in an MVC project (that I can do easily) but there do not seem to be anything specific towards taking the AccountController.cs functionality that comes out of the box in an MVC project and emulating/providing those actions as a service through WCF. Mainly the Authentication of users.
I want to use the WCF service to completely separate the web application layer from my data base layer (classic 3 layer architecture). All of the Guides i've seen seem to imply having to make your own custom PasswordValidator and or modifying the UserStores or rolling your own custom authentication scheme (is that right? am I missing something simpler?).
It seems like there would be a straight forward way of generating or enabling the same ASP.NET Entity Framework that gets generated at the start of an MVC project in a WCF project but I don't understand how to tie the ASP.Net Identity authorization like it's used in the MVC project into the WCF service. How can I make the service authorize user credentials against the identity database created and populated using the MVC site?
That is essentially what I am having trouble with. After I can get that working I want to add the service reference to the web application and use those service requests to load arbitrary data through my controller methods and use them in the Views, but that part I do understand how to do, I've done that before.
Examples i've looked at. https://msdn.microsoft.com/en-us/library/ff647503.aspx http://www.codeproject.com/Articles/802435/Authentication-and-Authorization-with-ASP-NET-Iden
I'm at the point where I'm willing to abandon trying to find the straight forward way of doing this and just Implement my own authentication, storing hashed passwords in a table with the salt needed to compare user's passwords with what they type in and just bypass this issue. It would mean re-inventing the wheel however and re-doing all of the AccountController.cs methods and IdentityConfig.cs and figuring out how to configure web.config to allow custom authentication. I thought the whole point of these VisualStudio tools and the framework is to not do that though.
Is there something that i'm missing or misunderstanding? Am I supposed to make my own custom Authentication Provider?
Any help would be greatly appreciated. I also saw articles about STS and WIF with WCF but I don't think that is what I'm after at the moment.
Upvotes: 1
Views: 1623
Reputation: 31
I ended up implementing my own IdentityUser class and interface. Then used that as a base to write custom UserStore, UserTable, ApplicationUserManager and AppPassword classes in my app that made use of reference to a WCF service to get the necessary data
using System;
using System.Data;
using System.Data.SqlClient;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using System.Security.Cryptography;
using System.Text;
namespace ProudSourcePrime.Identity
{
/// <summary>
/// Class that represents the Users table in our database.
///
/// It actually does not connect to our database anymore.
///
/// The data it recives for this implementation comes from a service reference running on our data service server.
/// </summary>
/// <typeparam name="TUser"></typeparam>
public class UserTable<TUser> where TUser : IdentityUser
{
/// <summary>
/// sql query that will retrive the username of this user account, keying off of the userId
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public string GetUserName(string userId)
{
string userName = null;
// TODO : sql query that will retrive the username of this user account, keying off of the userId
return userName;
}
/// <summary>
/// sql query that will retrive the userId of this user account, keying off of the userName
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public string GetUserId(string userName)
{
string userId = null;
// TODO : sql query that will retrive the userId of this user account, keying off of the userName
return userId;
}
/// <summary>
/// sql query to get our user data
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public TUser GetUserById(string userId)
{
TUser user = null;
ServiceReference1.Service1Client ProudSoureService = new ServiceReference1.Service1Client();
ServiceReference1.UserRecordComposite userComposite = ProudSoureService.get_UserById(userId);
user = (TUser)Activator.CreateInstance(typeof(TUser));
user.AccessFailedCount = userComposite.AccessFailedCount;
user.Email = userComposite.Email;
user.Id = userComposite.Id;
user.LockoutEnabled = userComposite.LockoutEndDateUtc;
user.Name = userComposite.Name;
user.PasswordHash = userComposite.PasswordHash;
user.PhoneNumber = userComposite.PhoneNumber;
user.PhoneNumberConfirmed = userComposite.PhoneNumberConfirmed;
user.SecurityStamp = userComposite.SecurityStamp;
user.TwoFactorEnabled = userComposite.TwoFactorEnabled;
user.UserName = userComposite.UserName;
return user;
}
/// <summary>
/// sql query to retrive user using username
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public TUser GetUserByUserName(string userName)
{
TUser user = null;
ServiceReference1.Service1Client ProudSourceService = new ServiceReference1.Service1Client();
ServiceReference1.UserRecordComposite userComposite = ProudSourceService.get_UserByUserName(userName);
user = (TUser)Activator.CreateInstance(typeof(TUser));
user.AccessFailedCount = userComposite.AccessFailedCount;
user.Email = userComposite.Email;
user.Id = userComposite.Id;
user.LockoutEnabled = userComposite.LockoutEndDateUtc;
user.Name = userComposite.Name;
user.PasswordHash = userComposite.PasswordHash;
user.PhoneNumber = userComposite.PhoneNumber;
user.PhoneNumberConfirmed = userComposite.PhoneNumberConfirmed;
user.SecurityStamp = userComposite.SecurityStamp;
user.TwoFactorEnabled = userComposite.TwoFactorEnabled;
user.UserName = userComposite.UserName;
return user;
}
/// <summary>
/// sql query for password hash of this user keying off of the userId
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public string GetPasswordHash(string userId)
{
return new ServiceReference1.Service1Client().get_PasswordHash(userId);
}
/// <summary>
/// Sql command that actually created the user record and inserts into it the passwordHash, usernam and Guid Id.
/// </summary>
/// <param name="user"></param>
/// <param name="passwordHash"></param>
/// <returns></returns>
public bool SetPasswordHash(TUser user, string passwordHash)
{
return new ServiceReference1.Service1Client().set_PasswordHash(user.Id, passwordHash, user.UserName, user.Name);
}
/// <summary>
/// Sql command that actually creates the user record and inserts into it the passwordHash, and Guid Id.
///
/// No in use currently SetPasswordHash(TUser user, string passwordHash) gets called.
/// </summary>
/// <param name="userId"></param>
/// <param name="passwordHash"></param>
/// <returns></returns>
public bool SetPasswordHash(string userId, string passwordHash)
{
bool result = false;
//
return result;
}
/// <summary>
/// sql query that retrives the security stamp for this user record
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public string GetSecurityStamp(string userId)
{
string securityStamp = null;
// TODO : sql query that retrives the security stamp for this user record
return securityStamp;
}
/// <summary>
/// sql query that will update a Users table record with the given security stamp
/// </summary>
/// <param name="userId"></param>
/// <param name="securityStamp"></param>
/// <returns></returns>
public bool SetSecurityStamp(string userId, string securityStamp)
{
bool result = false;
// TODO : sql query that sets the security stamp of a user record
return result;
}
/// <summary>
/// sql query that inserts a new user into our data base
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public bool Insert(TUser user)
{
bool result = false;
// TODO : sql query that inserts a new User entry
return result;
}
/// <summary>
/// sql query to delete this user from our table
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public bool Delete(TUser user)
{
bool result = false;
// TODO : sql query to delete this user from our table
return result;
}
/// <summary>
/// sql query that will update our user record on our Users table
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public bool Update(TUser user)
{
bool result = false;
// TODO : sql query that will update our user record on our Users table
return result;
}
}
/// <summary>
/// Class that implements ASP.NET IUserPasswordStore, IUserSecurityStamp and IUserStore off of the concrete impemntations of IdentityUser's methods.
/// </summary>
/// <typeparam name="TUser"></typeparam>
public class UserStore<TUser> : IUserStore<TUser>, IUserPasswordStore<TUser> where TUser : IdentityUser
{
/// <summary>
/// Private resident that gives access to UserTable's concrete methods
/// </summary>
private UserTable<TUser> userTable;
/// <summary>
/// Default Constructor that initializes a new UserTable with connection to our data base.
/// </summary>
public UserStore()
{
userTable = new UserTable<TUser>();
}
/// <summary>
/// Insert a new user into our Users table.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task CreateAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
// This is commented out for now
//
// New User is actually created in method SetPasswordHash(TUser user, string passwordHash).
//userTable.Insert(user);
return Task.FromResult<object>(null);
}
/// <summary>
/// Delete a user from our Users table.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task DeleteAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
userTable.Delete(user);
return Task.FromResult<object>(null);
}
void IDisposable.Dispose()
{
throw new NotImplementedException();
}
/// <summary>
/// Retrives a user by using an Id.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public Task<TUser> FindByIdAsync(string userId)
{
if (string.IsNullOrEmpty(userId))
{
throw new ArgumentNullException("user");
}
return Task.FromResult(userTable.GetUserById(userId));
}
/// <summary>
/// Find a user by using the username.
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public Task<TUser> FindByNameAsync(string userName)
{
if (string.IsNullOrEmpty(userName))
{
throw new ArgumentNullException("TUser is null");
}
return Task.FromResult(userTable.GetUserByUserName(userName));
}
/// <summary>
/// Returns the passwordhash for a given TUser
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<string> GetPasswordHashAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
return Task.FromResult(userTable.GetPasswordHash(user.Id));
}
/// <summary>
/// Get the security stamp for a given TUser.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<string> GetSecurityStampAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
return Task.FromResult(userTable.GetSecurityStamp(user.Id));
}
/// <summary>
/// Verfies whether a given TUser has a password.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task<bool> HasPasswordAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
if (!string.IsNullOrEmpty(userTable.GetPasswordHash(user.Id)))
{
return Task.FromResult(true);
}
else
{
return Task.FromResult(false);
}
}
/// <summary>
/// Sets the password hash for a given TUser.
/// </summary>
/// <param name="user"></param>
/// <param name="passwordHash"></param>
/// <returns></returns>
public Task SetPasswordHashAsync(TUser user, string passwordHash)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
if(userTable.SetPasswordHash(user, passwordHash))
{
return Task.FromResult("true");
}
else
{
return Task.FromResult("false");
}
}
/// <summary>
/// This method will set the secrity stamp for a given TUser.
/// </summary>
/// <param name="user"></param>
/// <param name="stamp"></param>
/// <returns></returns>
public Task SetSecurityStampAsync(TUser user, string stamp)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
if(userTable.SetSecurityStamp(user.Id, stamp))
{
return Task.FromResult("true");
}
else
{
return Task.FromResult("false");
}
}
/// <summary>
/// This method will update a given TUser
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public Task UpdateAsync(TUser user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
if(userTable.Update(user))
{
return Task.FromResult("true");
}
else
{
return Task.FromResult("false");
}
}
}
/// <summary>
/// Implementation of the UserManager class that will be handeling verification and
/// </summary>
public class ApplicationUserManager : UserManager<IdentityUser>
{
/// <summary>
/// Class instantiation.
/// </summary>
/// <param name="store"></param>
public ApplicationUserManager(UserStore<IdentityUser> store) : base(store)
{
Store = store;
this.PasswordHasher = new AppPassword();
UserLockoutEnabledByDefault = false;
// this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(10);
// this.MaxFailedAccessAttemptsBeforeLockout = 10;
UserValidator = new UserValidator<IdentityUser>(this)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = false
};
// Configure validation logic for passwords
PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = false,
RequireUppercase = false,
};
}
/// <summary>
/// Override that actually uses the base layer implementation thus exposing it's funtionality through this object.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public override System.Threading.Tasks.Task<IdentityResult> CreateAsync(IdentityUser user, string password)
{
return base.CreateAsync(user, password);
}
}
/// <summary>
/// Custom password hasher and hash comparison implementor.
/// </summary>
public class AppPassword : IPasswordHasher
{
/// <summary>
/// Method that hashes passwords.
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
public string HashPassword(string password)
{
using (SHA256 sha = SHA256Managed.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(password.ToString()));
StringBuilder hashSB = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
hashSB.Append(hash[i].ToString("x2"));
}
return hashSB.ToString();
}
}
/// <summary>
/// Method that compares a given password hash with an input password and compares the given password hash with the hash of the input password.
/// </summary>
/// <param name="hashedPassword"></param>
/// <param name="providedPassword"></param>
/// <returns></returns>
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
string providedPassword_hashed = HashPassword(providedPassword);
if (hashedPassword.Equals(providedPassword_hashed))
{
return PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
}
}
}
here is IdentityUser
using Microsoft.AspNet.Identity;
using System;
namespace ProudSourcePrime.Identity
{
/// <summary>
/// Class that implements ASP.NET Identity IUser interface.
/// </summary>
public class IdentityUser : IUser
{
public string Id { get; set; }
public string UserName { get; set; }
public virtual string Email { get; set; }
public virtual string PasswordHash { get; set; }
public virtual string SecurityStamp { get; set; }
public virtual string PhoneNumber { get; set; }
public virtual bool PhoneNumberConfirmed { get; set; }
public virtual bool TwoFactorEnabled { get; set; }
public virtual DateTime? LockoutEnabled { get; set; }
public virtual int AccessFailedCount { get; set; }
public virtual string Name { get; set; }
/// <summary>
/// Default constructor that generates a new guid.
/// </summary>
public IdentityUser()
{
Id = Guid.NewGuid().ToString();
}
/// <summary>
/// Constructor that accepts a Username as a parameter.
/// </summary>
/// <param name="userName"></param>
public IdentityUser(string userName) : this()
{
UserName = userName;
}
/// <summary>
/// Public accessor to this IdentityUsers GUID
/// </summary>
string IUser<string>.Id
{
get
{
return Id;
}
}
/// <summary>
/// Public accessor to this IdentityUsers UserName
/// </summary>
string IUser<string>.UserName
{
get
{
return UserName;
}
set
{
UserName = value;
}
}
}
}
Then I Implemented AppUserPrincipal which inherits ClaimsPrincipal to authorize claims and AppViewPage inheriting WebViewPage so all pages on the site are now an AppViewPage type but still inherit what is necessary to run appropriately
using System.Security.Claims;
using System.Web.Mvc;
namespace ProudSourcePrime.Config
{
public class AppUserPrincipal : ClaimsPrincipal
{
public AppUserPrincipal(ClaimsPrincipal principal) : base(principal)
{
}
public string Name
{
get
{
return this.FindFirst(ClaimTypes.Name).Value;
}
}
}
public abstract class AppController : Controller
{
public AppUserPrincipal CurrentUser
{
get
{
return new AppUserPrincipal(this.User as ClaimsPrincipal);
}
}
}
/// <summary>
/// Custom base view page to be inherited by all Razor Views in the web application.
///
/// This provides access too our AppUser principal.
/// </summary>
/// <typeparam name="TModel"></typeparam>
public abstract class AppViewPage<TModel> : WebViewPage<TModel>
{
protected AppUserPrincipal CurrentUser
{
get
{
return new AppUserPrincipal(this.User as ClaimsPrincipal);
}
}
}
public abstract class AppViewPage : AppViewPage<dynamic>
{
}
}
I then made a web.config not in the root directory but actually within the Views folder that for the MVC project and there I configure the site to use my AppViewPage Class
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<!--<pages pageBaseType="System.Web.Mvc.WebViewPage">-->
<pages pageBaseType="ProudSourcePrime.Config.AppViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.Optimization "/>
<add namespace="ProudSourcePrime" />
</namespaces>
</pages>
</system.web.webPages.razor>
<appSettings>
<add key="webpages:Enabled" value="false" />
<add key="owin:AppStartup" value="Startup"/>
</appSettings>
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
<system.web>
<compilation>
<assemblies>
<add assembly="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
</system.web>
</configuration>
Then I just had to make sure to add my service reference to the project and make sure that the service returns the data necessary and presto it was ready to go.
Here is a link to the project that is on my github. Before you go ohh noes there are internal IPs in here and sensetive information! I know and I don't care the Server environment it is housed in is sealed off from any changes not going through the public facing webserver and that webserver is the only thing reachable from the open web. Even I dont have access anymore and i did that on purpose so that no one could access it and it will go away soon.
This project was for a finTech startup that is now defunct since the three founders (myself and two others) decided to venture into other things.
In case you want to see the end result though go here, it will soon go away since i stoped paying for the account. http://proudsource.us/welcome
Upvotes: 2