可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am developing a web site in ASP.NET MVC 5 (using RC1 version currently). The site will use Facebook for user authentication and for retrieving initial profile data.
For the authentication system I am using the new OWIN based ASP.NET Identity engine (http://blogs.msdn.com/b/webdev/archive/2013/07/03/understanding-owin-forms-authentication-in-mvc-5.aspx), since it greatly simplifies the process of authenticating with external providers.
The problem is that once a user first logs in, I want to get its email address from the Facebook profile, but this data is not included in the generated claims. So I have thought on these alternatives to get the address:
Instruct the ASP.NET Identity engine to include the email address in
the set of data that is retrieved from Facebook and then converted
to claims. I don't know if this is possible at all.
Use the Facebook graph API
(https://developers.facebook.com/docs/getting-started/graphapi) to
retrieve the email address by using the Facebook user id (which is
included in the claims data). But this will not work if the user has
set his email address as private.
Use the Facebook graph API, but specifying "me" instead of the
Facebook user id
(https://developers.facebook.com/docs/reference/api/user). But an
access token is required, and I don't know how to (or if it's
possible at all) retrieve the access token that ASP.NET uses to
obtain the user data.
So the question is:
How can I instruct the ASP.NET Identity engine to retrieve
additional information from Facebook and include it in the claims
data?
Or alternatively, how can I retrieve the generated access token so
that I can ask Facebook myself?
Thank you!
Note: for the authentication system my application uses code based on the sample project linked in this SO answer: https://stackoverflow.com/a/18423474/4574
回答1:
To retrieve additional information from facebook you can specify scopes you would like to include when you configure the facebook authentication options. Getting the additional information that's retrieved can be achieved by implementing the provider's OnAuthenticated method like this:
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
{
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
// All data from facebook in this object.
var rawUserObjectFromFacebookAsJson = context.User;
// Only some of the basic details from facebook
// like id, username, email etc are added as claims.
// But you can retrieve any other details from this
// raw Json object from facebook and add it as claims here.
// Subsequently adding a claim here will also send this claim
// as part of the cookie set on the browser so you can retrieve
// on every successive request.
context.Identity.AddClaim(...);
return Task.FromResult(0);
}
}
};
//Way to specify additional scopes
facebookOptions.Scope.Add("...");
app.UseFacebookAuthentication(facebookOptions);
Per the code here i see the email is already retrieved and added as a claim here if facebook has sent. Are you not able to see it?
回答2:
Create a new Microsoft.Owin.Security.Facebook.AuthenticationOptions object in Startup.ConfigureAuth (StartupAuth.cs), passing it the FacebookAppId, FacebookAppSecret, and a new AuthenticationProvider. You will use a lambda expression to pass the OnAuthenticated method some code to add Claims to the identity which contain the values you extract from context.Identity. This will include access_token by default. You must add email to the Scope. Other user properties are available from context.User (see link at bottom for example).
StartUp.Auth.cs
// Facebook : Create New App
// https://dev.twitter.com/apps
if (ConfigurationManager.AppSettings.Get("FacebookAppId").Length > 0)
{
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
{
AppId = ConfigurationManager.AppSettings.Get("FacebookAppId"),
AppSecret = ConfigurationManager.AppSettings.Get("FacebookAppSecret"),
Provider = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, XmlSchemaString, "Facebook"));
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:email", context.Email, XmlSchemaString, "Facebook"));
return Task.FromResult(0);
}
}
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
}
In AccountController, I extract the ClaimsIdentity from the AuthenticationManager using the external cookie. I then add it to the identity created using the application cookie. I ignored any claims that starts with "...schemas.xmlsoap.org/ws/2005/05/identity/claims" since it seemed to break the login.
AccountController.cs
private async Task SignInAsync(CustomUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
// Extracted the part that has been changed in SignInAsync for clarity.
await SetExternalProperties(identity);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
private async Task SetExternalProperties(ClaimsIdentity identity)
{
// get external claims captured in Startup.ConfigureAuth
ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
if (ext != null)
{
var ignoreClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims";
// add external claims to identity
foreach (var c in ext.Claims)
{
if (!c.Type.StartsWith(ignoreClaim))
if (!identity.HasClaim(c.Type, c.Value))
identity.AddClaim(c);
}
}
}
And finally, I want to display whatever values are not from the LOCAL AUTHORITY. I created a partial view _ExternalUserPropertiesListPartial that appears on the /Account/Manage page. I get the claims I previously stored from AuthenticationManager.User.Claims and then pass it to the view.
AccountController.cs
[ChildActionOnly]
public ActionResult ExternalUserPropertiesList()
{
var extList = GetExternalProperties();
return (ActionResult)PartialView("_ExternalUserPropertiesListPartial", extList);
}
private List<ExtPropertyViewModel> GetExternalProperties()
{
var claimlist = from claims in AuthenticationManager.User.Claims
where claims.Issuer != "LOCAL AUTHORITY"
select new ExtPropertyViewModel
{
Issuer = claims.Issuer,
Type = claims.Type,
Value = claims.Value
};
return claimlist.ToList<ExtPropertyViewModel>();
}
And just to be thorough, the view:
_ExternalUserPropertiesListPartial.cshtml
@model IEnumerable<MySample.Models.ExtPropertyViewModel>
@if (Model != null)
{
<legend>External User Properties</legend>
<table class="table">
<tbody>
@foreach (var claim in Model)
{
<tr>
<td>@claim.Issuer</td>
<td>@claim.Type</td>
<td>@claim.Value</td>
</tr>
}
</tbody>
</table>
}
The working example and complete code is on GitHub: https://github.com/johndpalm/IdentityUserPropertiesSample
And any feedback, corrections, or improvements would be appreciated.
回答3:
It worked for me with just this in Startup.Auth
:
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions() {
AppId = "*",
AppSecret = "**"
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
And then in the method ExternalLoginCallback
or ExternalLoginConfirmation
you get the email with :
ClaimsIdentity ext = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var email = ext.Claims.First(x => x.Type.Contains("emailaddress")).Value;
回答4:
You need to create an instance of FacebookAuthenticationOptions
and configurethe Provider
. The Provider
contains an event called OnAuthenticated
that is triggered when you login.
var facebookOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = (context) =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:access_token", context.AccessToken, ClaimValueTypes.String, "Facebook"));
return Task.FromResult(0);
}
},
// You can store these on AppSettings
AppId = ConfigurationManager.AppSettings["facebook:AppId"],
AppSecret = ConfigurationManager.AppSettings["facebook:AppSecret"]
};
app.UseFacebookAuthentication(facebookOptions);
In the above code I am accessing the access_token
by context.AccessToken
and add it to Claims
of the current loggedin user.
To access this value later you need to do this:
var owinContext = HttpContext.GetOwinContext();
var authentication = owinContext.Authentication;
var user = autentication.User;
var claim = (user.Identity as ClaimsIdentity).FindFirst("urn:facebook:access_token");
string accessToken;
if (claim != null)
accessToken = claim.Value;
To simplify all of this, you can create a BaseController
and make all your Controllers
inherit from it.
The BaseController
code would be:
public class BaseController : Controller
{
public IOwinContext CurrentOwinContext
{
get
{
return HttpContext.GetOwinContext();
}
}
public IAuthenticationManager Authentication
{
get
{
return CurrentOwinContext.Authentication;
}
}
public new ClaimsPrincipal User
{
get
{
return Authentication.User;
}
}
public ClaimsIdentity Identity
{
get
{
return Authentication.User.Identity as ClaimsIdentity;
}
}
public string FacebookAccessToken
{
get
{
var claim = Identity.FindFirst("urn:facebook:access_token");
if (claim == null)
return null;
return claim.Value;
}
}
}
Then to get the access token on your code you just need to access the property FacebookAccessToken
.
string accessToken = FacebookAccessToken;
It is possible to retrieve some other values as
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:username",
context.User.Value<string>("username"), ClaimValueTypes.String, "Facebook"));
context.Identity.AddClaim(new System.Security.Claims.Claim("urn:facebook:name",
context.User.Value<string>("name"), ClaimValueTypes.String, "Facebook"));
Note that not all fields will be available, to get the email you need to require the Scope
email.
facebookOptions.Scope.Add("email");
Then access on the OnAuthenticated
event as
context.User.Value<string>("email");
回答5:
Here are some steps which will help you. I am in the process of writing up a blog post but it will take a while...
- Add Scopes in Fb provider and add the data returned from FB as claims
app.UseFacebookAuthentication(new FacebookAuthenticationOptions()
{
AppId = "",
AppSecret = "",
//Scope = "email,user_about_me,user_hometown,friends_about_me,friends_photos",
Provider = new FacebookAuthenticationProvider()
{
OnAuthenticated = async context =>
{
foreach (var x in context.User)
{
context.Identity.AddClaim(new System.Security.Claims.Claim(x.Key, x.Value.ToString()));
}
//Get the access token from FB and store it in the database and use FacebookC# SDK to get more information about the user
context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
}
},
SignInAsAuthenticationType = "External",
});
Use the Access Token and call Facebook C# SDK to get the list of friends for the user
var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
var access_token = claimsIdentity.FindAll("FacebookAccessToken").First().Value;
var fb = new FacebookClient(access_token);
dynamic myInfo = fb.Get("/me/friends");
var friendsList = new List<FacebookViewModel>();
foreach (dynamic friend in myInfo.data)
{
friendsList.Add(new FacebookViewModel() { Name = friend.name, ImageURL = @"https://graph.facebook.com/" + friend.id + "/picture?type=large" });
//Response.Write("Name: " + friend.name + "<br/>Facebook id: " + friend.id + "<br/><br/>");
}
回答6:
my couple cents to all answers... if you still want to ask Facebook yourself, it makes sense to take a look on already existing Facebook package. It provides some great functionality that already implemented, so you don't have to re-implement it yourself... some example how to use it inside ASP.NET MVC application you can find here.