I am using the following code from MVC Storefront to test OpenId in MVC. How do I integrate it with my ASP.Net Membership so I can use roles and save a user name for the user in my tables? I believe that SO is also using something similar.
public ActionResult OpenIdLogin()
{
string returnUrl = VirtualPathUtility.ToAbsolute("~/");
var openid = new OpenIdRelyingParty();
var response = openid.GetResponse();
if (response == null)
{
// Stage 2: user submitting Identifier
Identifier id;
if (Identifier.TryParse(Request["openid_identifier"], out id))
{
try
{
IAuthenticationRequest req = openid.CreateRequest(Request["openid_identifier"]);
var fetch = new FetchRequest();
//ask for more info - the email address
var item = new AttributeRequest(WellKnownAttributes.Contact.Email);
item.IsRequired = true;
fetch.Attributes.Add(item);
req.AddExtension(fetch);
return req.RedirectingResponse.AsActionResult();
}
catch (ProtocolException ex)
{
ViewData["Message"] = ex.Message;
return View("Logon");
}
}
else
{
ViewData["Message"] = "Invalid identifier";
return View("Logon");
}
}
else
{
// Stage 3: OpenID Provider sending assertion response
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
var fetch = response.GetExtension<FetchResponse>();
string name = response.FriendlyIdentifierForDisplay;
if (fetch != null)
{
IList<string> emailAddresses = fetch.Attributes[WellKnownAttributes.Contact.Email].Values;
string email = emailAddresses.Count > 0 ? emailAddresses[0] : null;
//don't show the email - it's creepy. Just use the name of the email
name = email.Substring(0, email.IndexOf('#'));
}
else
{
name = name.Substring(0, name.IndexOf('.'));
}
//FormsAuthentication.SetAuthCookie(name, false);
SetCookies(name, name);
AuthAndRedirect(name, name);
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
case AuthenticationStatus.Canceled:
ViewData["Message"] = "Canceled at provider";
return View("Logon");
case AuthenticationStatus.Failed:
ViewData["Message"] = response.Exception.Message;
return View("Logon");
}
}
return new EmptyResult();
}
ActionResult AuthAndRedirect(string userName, string friendlyName)
{
string returnUrl = Request["ReturnUrl"];
SetCookies(userName, friendlyName);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
There are several questions like yours already on StackOverflow. This one seems particularly similar.
If you're already using the Membership provider for your site and are just adding OpenID to it, then I guess you're stuck with Membership for now and can use one of the answers to the question I linked to to get a semi-decent membership provider that MAY work for you.
But if you're writing a new site and just want "use roles and save a user name for the user in my tables" as you said, then DON'T use ASP.NET Membership at all. It's SO not worth it! It doesn't fit OpenID's password-less paradigm and just causes more grief than anything else. If you're not afraid of a little bit of database access yourself, do it that way. And you can get Roles behavior very easily by just issuing your own FormsAuthentication.RedirectFromLoginPage or FormsAuthentication.SetAuthCookie call and passing in the roles the user fills.
The open id provider will return data about the user. If you don't request/require specific tokens of information, then all you'll be given is the user's display name and identity URL.
Depending on what open id library you're using, you can request tokens like FirstName LastName, DOB (if you really cared) and if the user provided that information on their chosen identity, then you'd get it returned to you.
You can then use this to create a new user in the membership system. You'll probably have to give them a dummy password to get around the requirements of the Membership API.
To validate a login, provide 1 form that takes username & password and the other that takes an identity URL. After you've validated the user via open id, try to find the user by username (identity url) in the Membership API. If it doesn't exist, create it.
Related
I have just followed instructions in this article to add Google as a login provider to my MVC 5 app. All seems to work OK, but when I log in via Google, it wants me to register the email/username provided by Google as a new account in my app. If I leave the email as is and click the 'Register' button, it tells me that address is already taken, as I have earlier registered on my app's own login provider.
How can I tweak the default code generated by the MVC project template to allow me to associate the Google login with an existing local account?
P.S. I have exactly the same problem with Facebook.
I totally second the points raised by #Chris Pratt
However i'm not sure the code used is enough to do what the OP asked.
adding this code in the default block inside ExternalLoginCallback should do the job
ApplicationUser user = await UserManager.FindByNameAsync(loginInfo.Email);
if (user != null)
{
var addLoginResult = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
if (addLoginResult.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
I think Identity handles it the way it does on purpose, since there's no real way to verify the identity of the user by email alone coming from a third party. While the risk may be relatively low, it is possible that someone could create an account with a third party like Facebook, using an email address that does not belong to them, and then use that third-party login to impersonate an account at another website attached to the same email.
As a result, Identity only lets you create a new account with an external login when signing in, not attach to an existing one. However, once the user is authenticated by other means, methods are provided to associate additional logins.
If you're not concerned by the relatively mild security risk associated with just assuming that if the email matches it's the same person, then you need only modify ExternalLoginCallback in AccountController.cs to attempt to find the user by email:
var user = await UserManager.FindByEmailAsync(loginInfo.Email);
And then sign them in:
await SignInManager.SignInAsync(user);
Here is how I was able to solve this issue that you are having. This solution will allow you to register on the site with your email and if you then try to use Google to log in with the same Email address, you will not be requested to register and you will not generate any errors; you will be allowed to login if the email address that you logged in with locally is the same as your google account email. I edited the Default ExternalLoginCallBack code that VS2015 generated with an if / else statement, which is checking for an existing email that matches the login Email. I hope this helps you with your question, for I had the same issue and could not find an answer anywhere. My multiple post requests were ignored and thankfully, I read one of the answers from this post that led me to my own solution that is working for me on VS2015 Core.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
return View(nameof(Login));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
var user = await _userManager.FindByNameAsync(email);
if (user != null)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
// If user does not already exists, invite User to register.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
_logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
}
TLDR you need to go through all scenarios manually in your ExternalLoginConfirmation function, and have a database table to be able to match membership user id with OAuth user id. This way you can "associate" multiple OAuth accounts with single local account.
Below is a code snippet from one of our projects - hopefully it's clear enough
public ActionResult ExternalLoginCallback()
{
var returnUrl = HttpContext.Request.QueryString["returnUrl"];
var result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
if (result.IsSuccessful == false)
{
return this.View("ExternalLoginFailure", result);
}
// Login user if provider represents a valid already registered user
if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false))
{
return this.RedirectToLocal(returnUrl);
}
// If the current user is logged in already - add new account
if (User.Identity.IsAuthenticated)
{
OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, User.Identity.Name);
return this.RedirectToLocal(returnUrl);
}
var membershipUser = Membership.GetUser(result.UserName);
// so user is new - then create new membership account
if (membershipUser == null)
{
MembershipCreateStatus createStatus;
membershipUser = Membership.CreateUser(username: result.UserName, password: this.GetTempPassword(), email: result.UserName, status: out createStatus);
if (createStatus == MembershipCreateStatus.Success)
{
this.emailService.SendWelcome(this, (Guid)membershipUser.ProviderUserKey);
// Associate social network account with created membership account
OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, result.UserName);
OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false);
return this.RedirectToLocal(returnUrl);
}
// The problem occured while creating membership account
this.ViewBag.Error = MembershipErrorNameProvider.FromErrorCode(createStatus);
return this.View("CreateMembershipAccountFailure");
}
// If membership account already exists -> Associate Social network account with exists membership account
OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, result.UserName);
OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false);
return this.RedirectToLocal(returnUrl);
}
and OAuthWebSecurity is a helper class which deals with all providers you support:
public static class OAuthWebSecurity
{
....
public static bool Login(string providerName, string providerUserId, bool createPersistentCookie)
{
var context = new HttpContextWrapper(HttpContext.Current);
var provider = GetOAuthClient(providerName);
var securityManager = new OpenAuthSecurityManager(context, provider, OAuthDataProvider);
return securityManager.Login(providerUserId, createPersistentCookie);
}
public static void CreateOrUpdateAccount(string openAuthProvider, string openAuthId, string userName)
{
var user = UserRepository.FindByName(userName);
if (user == null)
{
throw new MembershipUserNotFoundException();
}
var userOAuthAccount = UserOAuthAccountRepository.Find(openAuthProvider, openAuthId);
if (userOAuthAccount == null)
{
UserOAuthAccountRepository.InsertOrUpdate(new UserOAuthAccount
{
OAuthProvider = openAuthProvider,
OAuthId = openAuthId,
UserId = user.Id
});
}
else
{
userOAuthAccount.UserId = user.Id;
}
UserOAuthAccountRepository.Save();
}
}
This is a normal behavior in MVC template, since external logins attempt to create user and if the user's email (or external identity) already exists, attempt to signin. Furthermore, external login provider tries to assign an optional and unique identifier in your application (locally) separate from your external identity. But, the following is strange, as you said:
If I leave the email as is and click the 'Register' button, it tells
me that address is already taken, as I have earlier registered on my
app's own login provider.
Which should work since each user's external id should be unique in other sites (I believe) unless you have registered multiple external accounts with the same identity since the table structures looks like this:
The column UserId will be matched with the bellow table's UserId column:
The message will be showed rarely, when a user tries to assign duplicate Username which will be enumerated with your email or identity name in other sites (e.g. somename#gmail.com will be somename as Username)
This following link shows you how to build an ASP.NET MVC 5 web application that enables users to log in using OAuth 2.0 with credentials from an external authentication provider, such as Facebook, Twitter, LinkedIn, Microsoft, or Google. For simplicity, this tutorial focuses on working with credentials from Facebook and Google existing accounts.
Refer this:
MVC 5 App with Facebook, Twitter, LinkedIn and Google OAuth2 Sign-on (C#)
We override the basic authentication in an MVC3 application by calling a webservice with the user's credentials and returning a WCF structure that contains the user's ID, a "LogonTicket". This LogonTicket is used to "authenticate the user for each call made to the webservice.
Now, we override by replacing the defaultProvider in the Web.config. All we do in this overridden provider is
to override the ValidateUser() function. That is where we call the web service with their credentials and return
the "LogonTicket".
This is the LogOn() function from our AccountController, essentially the base code from the template:
public ActionResult LogOn(LogOnModel model)
{
string ReturnUrl = "";
if (HttpContext.Request.UrlReferrer.Query.Length > 11)
{
ReturnUrl = Uri.UnescapeDataString(HttpContext.Request.UrlReferrer.Query.Substring(11));
}
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
&& !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
{
return Redirect(ReturnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
ViewBag.MainWebsite = MainWebsite;
return View(model);
}
This is the overridden ValidateUser() function from our new default provider:
public override bool ValidateUser(string username, string password)
{
MyServiceClient mps = new MyServiceClient();
string sha1password = HashCode(password);
LogonInfo logonInfo = mps.GetLogonTicket(username, sha1password);
if (logonInfo.LogonTicket != "" && logonInfo.LogonTicket != "0")
{
// Authenticated so set session variables
HttpContext.Current.Session["LogonTicket"] = logonInfo.LogonTicket;
HttpContext.Current.Session["ParticipantID"] = logonInfo.ParticipantID;
return true;
}
else
{
return false;
}
}
I'm not really sure how to combine the use of the two, so my questions are:
How can I implement OpenID and Facebook logins and keep my current authentication method?
How can we "map" the OpenID user with our current user DB values? We MUST know so we can retrieve their info.
I know we can retrieve their email address but what if their OpenID email is different than the one they use for their record on our site?
Are there any examples of how to do this, anywhere?
Thanks for looking at my question.
I have done a project which required multiple log-on possibilities (custom account, Google and Facebook)
In the end your authentication with ASP.NET is entirely dependant on your configuration. (In your case it is FormsAuthentication) this means that FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); basicly determines everything in regard to your user and where you set this isn't restricted.
You have now basicly the same implementation as we started out with, using a MembershipProvider to handle your own custom account. You only need to expand now to facilitate the openIds. You would have to expand your Controller with various actions for each login type (Now you have ActionResult LogOn() you can add to that for example: ActionResult LogOnOpenId()). Inside that method you basicly call the same code but instead of Membership.ValidateUser(model.UserName, model.Password) you call the OpenId services.
I have provided below an example of our google implementation using dotnetopenauth. The service method uses formsService.SignIn(userId.Value.ToString(), false); which basicly calls FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); (we only do some custom behaviour there in regard to the SecurityPrincipal but this doesn't affect your Authentication process). You can also see that we make a new account when we receive a new user. To solve your question part 2 we have implemented a profile which can be merged if you can provide another login. This allows our users to keep their account consolidated and use whatever login method they like.
For examples in regard to multiple signons I will refer to the answer of Tomas whom referenced StackExchange as a good example. Also I'd advise you to install MVC4 and VS2012 and just do a File > New Project. The newest default template of MVC includes openid implementation alongside a custom login!
Example google openid implementation:
The controller method:
public virtual ActionResult LoginGoogle(string returnUrl, string runAction)
{
using (var openId = new OpenIdRelyingParty())
{
IAuthenticationResponse response = openId.GetResponse();
// If we have no response, start
if (response == null)
{
// Create a request and redirect the user
IAuthenticationRequest req = openId.CreateRequest(WellKnownProviders.Google);
var fetch = new FetchRequest();
fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
fetch.Attributes.AddRequired(WellKnownAttributes.Preferences.Language);
req.AddExtension(fetch);
req.RedirectToProvider();
return null;
}
_service.ConnectViaGoogle(response, TempData);
}
The service method:
public void ConnectViaGoogle(IAuthenticationResponse response, TempDataDictionary tempData)
{
// We got a response - check it's valid and that it's me
if (response.Status == AuthenticationStatus.Authenticated)
{
var claim = response.GetExtension<FetchResponse>();
Identifier googleUserId = response.ClaimedIdentifier;
string email = string.Empty;
string firstName = string.Empty;
string lastName = string.Empty;
string language = string.Empty;
if (claim != null)
{
email = claim.GetAttributeValue(WellKnownAttributes.Contact.Email);
firstName = claim.GetAttributeValue(WellKnownAttributes.Name.First);
lastName = claim.GetAttributeValue(WellKnownAttributes.Name.Last);
language = claim.GetAttributeValue(WellKnownAttributes.Preferences.Language);
}
//Search User with google UserId
int? userId = _userBL.GetUserIdByGoogleSingleSignOnId(googleUserId);
//if not exists -> Create
if (!userId.HasValue)
{
_userBL.CreateGoogleUser(
googleUserId,
firstName,
lastName,
email,
language,
DBConstants.UserStatus.DefaultStatusId,
out userId);
}
if (userId.HasValue)
{
_userBL.UpdateLastLogon(userId.Value);
var formsService = new FormsAuthenticationService();
formsService.SignIn(userId.Value.ToString(), false);
AfterLoginActions(tempData);
}
}
}
Any questions or comments? I'll gladly hear them.
it should be perfectly possible to have multiple authentications methods. All IIS / ASP.net cares about is the FormsAuthentication cookies. So you would have one set of actions for your standard username/password auth, and another for OpenId. This is at least what I have done on one project.
You can't even trust the openId provider to give you an email address! A common solution to this problem is to allow a user to attach multiple OpenId identifiers (URI's) to the his account after logging in. This is e.g. how StackOverflow works. If this is the first time the user visits the system then you can auto create a new account, or force the user through a signup process.
When I added the OpenId support in the system mentioned, it had an existing table used to store username and password(users table). I added a new table with a many to one relationship with the users table, and used this to store the URI's.
As mentioned above StackOverflow it self is a good place to start, also there are a lot of good examples in the http://www.dotnetopenauth.net/ project.
As far as I know the source of SO is not public, and they are using the dotnetopenauth project.
This may be to abstract, but this library is a openId (among other things) for the open source orchard CMS: http://orchardopenauth.codeplex.com/
I hope this helps, but if you have any questions then please expand your question with more details.
Many days ago, I asked this question and I'm set with a working offline OpenID provider (I guess).
What I want is a simple login text box for OpenID identifier which will automatically accept the user and consider the user as logged in, then I want him to be redirected to the main products page (Products => Index).
But, what I didn't know (and didn't find on the internet) is how to go on after the fake authentication process.
I tried to do this:
[HttpPost]
public ActionResult Login(string openid)//openid is the identifier taken from the login textbox
{
var rely = new OpenIdRelyingParty();
var req = rely.CreateRequest(openid);
req.RedirectToProvider();
return RedirectToAction("Index", "Products");
}
First of all, it is not being redirected in the 4th line (instead the login page is refreshed) and second, no authenticated user is really there, I mean when I checked User in Watch window, it is not null but Username is empty and there is no authenticated identity which, I guess, means that there is no cookies set.
P.S:
1. No exceptions are being thrown. But when I try to call FormsAuthentication.RedirectFromLogin() method I get an exception (Server cannot modify cookies after HTTP headers have been set).
2. Index action method is marked with [Authorize] attribute, and so when someone tries to browse Products it is redirected to the login screen, but when he logs in (fake login), shouldn't he be redirected back to Products page?
I tried this also:
[HttpPost]
public ActionResult Login(string openid)
{
var rely = new OpenIdRelyingParty();
return rely.CreateRequest(openid).RedirectingResponse.AsActionResult();
}
But no luck.
What did I miss? and how to make it work as expected? I can depend on myself but I need a decent documentation for OpenID especially for the offline local provider.
Check this example: OpenID and OAuth using DotNetOpenAuth in ASP.NET MVC
public ActionResult OpenId(string openIdUrl)
{
var response = Openid.GetResponse();
if (response == null)
{
// User submitting Identifier
Identifier id;
if (Identifier.TryParse(openIdUrl, out id))
{
try
{
var request = Openid.CreateRequest(openIdUrl);
var fetch = new FetchRequest();
fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
request.AddExtension(fetch);
return request.RedirectingResponse.AsActionResult();
}
catch (ProtocolException ex)
{
_logger.Error("OpenID Exception...", ex);
return RedirectToAction("LoginAction");
}
}
_logger.Info("OpenID Error...invalid url. url='" + openIdUrl + "'");
return RedirectToAction("Login");
}
// OpenID Provider sending assertion response
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
var fetch = response.GetExtension<FetchResponse>();
string firstName = "unknown";
string lastName = "unknown";
string email = "unknown";
if(fetch!=null)
{
firstName = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
lastName = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
email = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
}
// Authentication
FormsAuthentication.SetAuthCookie(userName: email, createPersistentCookie: false);
// Redirection
return RedirectToAction("Index", "Products");
case AuthenticationStatus.Canceled:
_logger.Info("OpenID: Cancelled at provider.");
return RedirectToAction("Login");
case AuthenticationStatus.Failed:
_logger.Error("OpenID Exception...", response.Exception);
return RedirectToAction("Login");
}
return RedirectToAction("Login");
}
Basically, your action method is called twice:
The first time by your form being submitted by the user.
The second time is a call back (redirect) from the OpenID provider.
This time, there will be a value for the response. If response.Status is valid, you can log your user in using the FormsAuthentication class and finally you can redirect him to your main products page.
I have an MVC3 application and I would like to give the users the ability to set preferences that would be enabled when the user logs in.
I really don't have any idea where to start with this and would really appreciate being pointed in the right direction. I did try some changes to the membership class but now I am thinking that's probably not the best way to go about things.
You could do it in a database (sounds like you might be using one at least with the out-of-the-box membership provider) once uniquely identifying a user. In that case, you may want to implement your own membership provider.
You have to do a little work to start implementing your own provider. If this is your only requirement, you might be able to avoid it by writing your own class that returns settings in a format of your choosing
public static class UserSettings
{
public static string GetSettings(IPrincipal user)
{
if(user.Identity.IsAuthenticated)
{
// dip into database using user.Identity.Name property
return "string with user settings";
// this also assumes user.Identity.Name is uniquely able
// to identify a user in your database!
}
return string.Empty;
}
}
Or, if the information is completely trivial, maybe you could implement a cookie representation of the user settings. This, of course, comes with all the caveats of using cookies, but you could avoid storing the information in a database
Anywhere you have an HttpContext you could grab the settings value like so:
if(HttpContext.Current != null)
{
string userSettings = HttpRequest.Current.Request.Cookies["NameOfCookie"];
}
You can use the FormsAuthentication cookie to store your user information and avoid accessing the database all the time. That cookie is encrypted and whatever information you're storing as safe as the user session itself. The only problem with the cookies is that they have a maximum size of 4K so, if your user info is massive then you might run into a problem. When I use the cookie approach I store my user data as a JSON and then deserialize that JSON on each page request. Here is my login controller logic (I'm using SimpleMembership but the approach is the same:
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, model.RememberMe))
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var user = _userLogic.GetItem(model.UserName);
if (user != null && user.IsActive)
{
var newAuthTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name, authTicket.IssueDate, authTicket.Expiration, authTicket.IsPersistent, JsonConvert.SerializeObject(user));
var newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newAuthTicket))
{
Expires = authCookie.Expires
};
Response.Cookies.Add(newCookie);
return RedirectToLocal(returnUrl);
}
WebSecurity.Logout();
ModelState.AddModelError("UserName", "This account has been deactivated.");
return View(model);
}
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
Notice the newAuthTicket creation and how user instance is passed to it as a JSON. After that all I have to do is desirialize this user object in my base controller's OnAuthorization method:
protected override void OnAuthorization(AuthorizationContext filterContext)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket != null)
{
var principal = new CustomPrincipal(HttpContext.User.Identity)
{
CurrentUserInfo = JsonConvert.DeserializeObject<User>(authTicket.UserData)
};
HttpContext.User = principal;
AppUser = principal.CurrentUserInfo;
ViewBag.AppUser = AppUser;
}
}
base.OnAuthorization(filterContext);
}
Create a new table in your database.
I've been looking at refactoring my login controller for better code readability. In doing so, I came across the Programmatic OpenID Relying Party example that looks like this
using DotNetOpenAuth.Messaging;
public ActionResult LogOn()
{
var openid = new OpenIdRelyingParty();
IAuthenticationResponse response = openid.GetResponse();
if (response != null)
{
switch (response.Status)
{
case AuthenticationStatus.Authenticated:
FormsAuthentication.RedirectFromLoginPage(
response.ClaimedIdentifier, false);
break;
case AuthenticationStatus.Canceled:
ModelState.AddModelError("loginIdentifier",
"Login was cancelled at the provider");
break;
case AuthenticationStatus.Failed:
ModelState.AddModelError("loginIdentifier",
"Login failed using the provided OpenID identifier");
break;
}
}
return View();
}
[System.Web.Mvc.AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string loginIdentifier)
{
if (!Identifier.IsValid(loginIdentifier))
{
ModelState.AddModelError("loginIdentifier",
"The specified login identifier is invalid");
return View();
}
else
{
var openid = new OpenIdRelyingParty();
IAuthenticationRequest request = openid.CreateRequest(
Identifier.Parse(loginIdentifier));
// Require some additional data
request.AddExtension(new ClaimsRequest
{
BirthDate = DemandLevel.NoRequest,
Email = DemandLevel.Require,
FullName = DemandLevel.Require
});
return request.RedirectingResponse.AsActionResult();
}
}
Now this looks heaps cleaner and easier to read than what I used from the downloadable examples. (I downloaded the most recent version and this is the example they give - which is the same example I built my app on 5 months ago.)
[ValidateInput(false)]
public ActionResult Authenticate(string returnUrl) {
var response = openid.GetResponse();
if (response == null) {
// Stage 2: user submitting Identifier
Identifier id;
if (Identifier.TryParse(Request.Form["openid_identifier"], out id)) {
try {
return openid.CreateRequest(Request.Form["openid_identifier"]).RedirectingResponse.AsActionResult();
} catch (ProtocolException ex) {
ViewData["Message"] = ex.Message;
return View("Login");
}
} else {
ViewData["Message"] = "Invalid identifier";
return View("Login");
}
} else {
// Stage 3: OpenID Provider sending assertion response
switch (response.Status) {
case AuthenticationStatus.Authenticated:
Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);
if (!string.IsNullOrEmpty(returnUrl)) {
return Redirect(returnUrl);
} else {
return RedirectToAction("Index", "Home");
}
case AuthenticationStatus.Canceled:
ViewData["Message"] = "Canceled at provider";
return View("Login");
case AuthenticationStatus.Failed:
ViewData["Message"] = response.Exception.Message;
return View("Login");
}
}
return new EmptyResult();
}
Now that example has too many if statements for my liking, and with the extra processing that I have to add in (activity logging and checking for new user or add to existing account), it starts getting really messy really quick.
Unfortunately, if I were to refactor my code to look more like the first example, I'm stuck on one small problem. How does the View interact with this? I mean, it's looking for openid.GetResponse(), but how do I submit that response?
Like I said, if I can get this working, it looks as though it'll be a lot cleaner than my current way.
You don't submit this response. The OpenID provider does when you click approve or cancel on their approval page. As far as I understand what's going on here is that for example Google returns a bunch of data via GET and then DotNetOpenAuth parses when you call openid.GetResponse().
I've published a heavily commented basic OpenID for MVC implementation that also features user registration. You can find it at http://mvcopenid.codeplex.com. It's still not as clean as the upper sample, but I find it quite clean. I will refactor that eventually, but I'll need to figure out how to get around MVC's model binder nicely, because I don't like code like Request.Form["openid_identifier"].