Associate a new Google or Facebook login with an existing account - asp.net-mvc

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#)

Related

Register and Login by Phone Number in Asp.net Core Identity

Is it possible to don't use Email to register and login in asp.net core identity?
Users just register by mobile number and login by SMS verification.
One possible way is to setup identity as two factor authentication. Instead of email use username to store the mobile number. To do this set RequireUniqueEmail = false in ApplicationUserManager.Create.
You'll need to add your own code to retrieve the number and validate it. Make sure it always has the same format as it should match the username.
In the code where the username is verified skip the password check (since password is null), but do check the number. Send an SMS with code and continue with the flow.
You can add your own logic to create and verify the code and how long it is valid.
If you don't rely in Username in your application, (Like you don't show it to user ever), then you can set Username to a guid and skip the email part.
For registration,
var identityUser = new User
{
Username= Guid.NewGuid(),
PhoneNumber= phoneNumber
};
var result = await _signInManager.UserManager.CreateAsync(identityUser, user.Password);
For Login,
var user = await _signInManager.UserManager.Users
.SingleOrDefaultAsync(x =>
x.NormalizedEmail == usernameParam
|| x.PhoneNumber== usernameParam);
if (user == null)
{
return Forbid();
}
var result = await _signInManager.CheckPasswordSignInAsync(user, passwordParam, false);
if (result.Succeeded)
{
// return new TokenResponse(token, refreshToken);
}

How to extend the Owin OAuth to accommodate the Application level login confirmation

I am currently working with MVC5 application which is using Owin OAuth authentication. I am looking forward to extend the login criteria where I have added couple of tables (Application table (ApplicationId(guid), ApplicationName(nvarchar) and ApplicationUserTable(id, ApplicationId(FK from Application table), UserId(FK column from ASPNetUsers table))) in my security DB.
Could please anyone give me some idea on how to access this ApplicationUserTable in Owin context so that I can verify first if user belong to a particular application? I have looked through quite a few examples but didn't find anything relevant to the particular scenario i am working with.
You could set your ApplicationId field as the ClientId, then you will have a different ClientId for each application.
When the user sends an authentication token request, in the GrantResourceOwnerCredentials method when you check the user credentials, check if the user belongs to the application that received ClientId represents.
In a simple way, it could be something like the following:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
...
var user = await _userService.GetUserAsync(context.UserName, context.Password, context.ClientId);
if (user == null)
{
context.SetError("invalid_grant", "The user name, password or clientId is incorrect.");
return;
}
...
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
And in the user service:
public async Task<User> GetUserAsync(string userName, string password, string clientId = null)
{
var user = await _userManager.FindAsync(userName, password);
if (user == null || (clientId != null && user.ApplicationUsers.Where(au => au.ApplicationId == clientId).Count() == 0))
{
return null;
}
return user;
}
In the ValidateClientAuthentication method you should validate the ClientId-Secret are in your Application table.
I hope it helps.

Using OpenID/OpenAuth in MVC3 app with overridden authentication method

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.

Redirect user after successful (fake) login in OpenID Offline Provider

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.

How to integrate OpenId with ASP.Net Membership in MVC

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.

Resources