Retrieving users Facebook email on OWIN login [duplicate] - asp.net-mvc

This question already has answers here:
Why new fb api 2.4 returns null email on MVC 5 with Identity and oauth 2?
(6 answers)
Closed 6 years ago.
I am learning how to work with OWIN external auth using the default MVC template in VS 2015. I enabled Facebook auth and added the scope "email", expecting the user's email to be returned by Facebook after the user is authenticated (according to this). Still, there is no email in the context.User JSON object and context.Email is also null.
This is the relevant code in Startup.Auth.cs
var facebookOptions = new FacebookAuthenticationOptions
{
AppId = "XXXXXX",
AppSecret = "XXXXXX",
Provider = new FacebookAuthenticationProvider
{
OnAuthenticated = context =>
{
// Retrieve the OAuth access token to store for subsequent API calls
var accessToken = context.AccessToken;
// Retrieve the username
var facebookUserName = context.UserName;
// WHY IS IT EMPTY?
var facebookEmail = context.Email;
// You can even retrieve the full JSON-serialized user
var serializedUser = context.User;
return Task.FromResult(0);
}
}
};
facebookOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookOptions);
Any ideas what is missing? Why is the facebook email address not being returned?

Turns out there has been a breaking change in Facebook API v 2.4 where you have to specify fields you want to retrieve. The graph request used to be:
https://graph.facebook.com/v2.3/me?access_token=XXXXX
but for performance reasons as of FB API v2.4 you also have to specify fileds you want to retrieve within the scope:
https://graph.facebook.com/v2.4/me?fields=id,name,email&access_token=XXXXX
Microsoft FB client implementation by default attaches access_token to the query string as "?access_token" which leads to the broken request (extra question mark ):
https://graph.facebook.com/v2.4/me?fields=id,name,email?access_token=XXXXX
So, to remedy that we need to use a custom BackchannelHttpHandler. First, we create the endpoint class:
public class FacebookBackChannelHandler : HttpClientHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!request.RequestUri.AbsolutePath.Contains("/oauth"))
{
request.RequestUri = new Uri(request.RequestUri.AbsoluteUri.Replace("?access_token", "&access_token"));
}
return await base.SendAsync(request, cancellationToken);
}
}
And then we provide it in facebook auth options along with explicitly specifying UserInformationEndpoint:
var facebookAuthOptions = new FacebookAuthenticationOptions
{
AppId = ConfigurationManager.AppSettings["FacebookAppId"],
AppSecret = ConfigurationManager.AppSettings["FacebookAppSecret"],
BackchannelHttpHandler = new FacebookBackChannelHandler(),
UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email",
Scope = { "email" }
<.....>
};
From: https://stackoverflow.com/a/32636149/3130094

Related

ASP.NET MVC with Azure B2C refresh ID token issue

I am developing an ASP.NET MVC app with Azure B2C authentication. It is required that, after the ID token expires (IIS session not expires), any subsequent action call should automatically refresh the ID token with the refresh token and then continue the execution without re-login.
Questions:
Does the solution make sense?
After refreshing the ID token and set the cookies, how can I redirect to the original url and continue execution without re-login?
Thanks, any idea is highly appreciated.
This is my code:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var refreshToken = HttpContext.Current.Request.Cookies["msal.refreshtoken"];
if (refreshToken != null && !string.IsNullOrEmpty(refreshToken.Value))
{
var newIdToken = TokenService.RefreshIdToken(refreshToken.Value);
var idTokenCookie = new HttpCookie("msal.idtoken", newIdToken)
{
Secure = true,
HttpOnly = true
};
HttpContext.Current.Response.Cookies.Set(idTokenCookie);
return;
}
}
// TokenService.RefreshIdToken
public static string RefreshIdToken(string refreshToken)
{
var policyName = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
var B2CDomain = ConfigurationManager.AppSettings["ida:B2CDomain"];
var tenant = ConfigurationManager.AppSettings["ida:Tenant"];
var clientId = ConfigurationManager.AppSettings["ida:ClientId"];
var clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
var tokenEndpointUri = $"https://{B2CDomain}/{tenant}/{policyName}/oauth2/v2.0/token";
var httpClient = new HttpClient();
var requestBodyDict = new Dictionary<string, string>
{
{ "grant_type" , "refresh_token" },
{ "client_id" , clientId },
{ "client_secret" , clientSecret },
{ "scope" , $"openid" },
{ "refresh_token" , refreshToken }
};
var request = new HttpRequestMessage
{
RequestUri = new Uri(tokenEndpointUri),
Method = HttpMethod.Post,
Content = new FormUrlEncodedContent(requestBodyDict)
};
var task = Task.Run(() => httpClient.SendAsync(request));
task.Wait();
var response = task.Result;
var task1 = Task.Run(() => response.Content.ReadAsStringAsync());
task1.Wait();
var responseString = task1.Result;
if (response.IsSuccessStatusCode)
{
var idToken = (string)JsonConvert.DeserializeObject<dynamic>(responseString).id_token.ToString();
return idToken;
}
else
{
throw new Exception();
}
}
A couple of thoughts that are too long to put in comments:
Yes the basic idea of ‘use the refresh token to get a new id token’ is how it’s supposed to work.
Googling this question suggests a bewildering array of examples to imitate :-( e.g. Microsoft’s Azure Samples on GitHub for A/D auth for a web app (as opposed to webapi or SPA)
The basic plan for identity problems like this is, find an authoritative example and follow it because that reduces your risk of embarrassing error. ( For instance, Auth0’s example for this scenario says to get a new refresh_token as well as a new id_token. Not doing that might be okay but then the user will be forced to re-login when the refresh token expires. Then you’ll be tempted to use ultra-long-lifetime refresh token, loosening your security a little)
If you can’t find an authoritative example, considering raising an issue or commenting on one.
OTOH, if the code you’ve written works, then maybe you’ve done!
The problem with finding an example to imitate after you’ve got started is trying to find just the right the example for the technology choices you already made. It may be easier to start with an empty project, follow a tutorial, get the tutorial working, then copy the solution back into your app.
To send your user back to their original target you should be able to
var originalUrl= HttpContext.Current.Request.Url;
HttpContext.Current.Response.Redirect(original);
But only do that if getting the id_token succeeded otherwise it creates an infinite loop.

Sign In using raw HttpRequestMessage in ASP.NET MVC

I have been testing some code to sign in users to their Microsoft/school/work accounts using raw HttpRequestMessage and HttpResponseMessage. I know there are libraries available to do this but I want to test the raw approach as well (especially usage of refresh tokens), while looking for the right library to handle it.
I'm currently learning authentication, with limited knowledge of ASP.NET/Core.
I'm following this guide: https://learn.microsoft.com/en-us/graph/auth-v2-user
I've just modified the SignIn() method in AccountController in an example project that used more high level libraries to sign in.
I'm requesting an authorization code.
The SignIn() code:
public void SignIn()
{
using (var httpClient = new HttpClient())
{
try
{
var tenant = "my tenant id";
var clientId = ConfigurationManager.AppSettings["ida:AppID"];
var responseType = "id_token+code";
var redirectURI = ConfigurationManager.AppSettings["ida:RedirectUri"];
var responseMode = "form_post";//query";
var appScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
var scopes = $"openid profile offline_access {appScopes}";
var state = "12345";
//var prompt = "consent";
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize", tenant);
var body = string.Format("client_id={1}&response_type={2}&redirect_uri={3}&response_mode={4}&scope={5}&state={6}", tenant, clientId, responseType, redirectURI, responseMode, scopes, state);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result;
var content = response.Content.ReadAsStringAsync().Result;
}
catch (Exception ex)
{
}
}
//if (!Request.IsAuthenticated)
//{
// // Signal OWIN to send an authorization request to Azure
// Request.GetOwinContext().Authentication.Challenge(
// new AuthenticationProperties { RedirectUri = "/" },
// OpenIdConnectAuthenticationDefaults.AuthenticationType);
//}
}
I'm just returning void from the method now because I'm not sure what I should return yet.
Debugging and looking at the response variable, the status code is 200, and has some other information to it. However, the content of the HttpResponseMessage, when I paste it into a file and opening it in a browser, displays (or redirects to) https://login.microsoftonline.com/cookiesdisabled, which shows a message saying that I could not be logged in because my browser blocks cookies. However, I don't think this really is the case.
How can I resolve this and have the user log in and consent, and get the authorization code?
I couldn't really find any example in ASP.NET that uses this raw approach. Is it not recommended?
You should fistly understand how OAuth 2.0 authorization code flow works in Azure AD V2.0 :
Microsoft identity platform and OAuth 2.0 authorization code flow
The general process would be like :
When login in client application, user will be redirect to Azure AD login endpoint(https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize) and provides info like which client(client_id) in which tenant(tenant id) user wants to login , and redirect back to which url(redirect_uri) after successful login.
User enter credential , Azure AD validate credential and issue code and redirect user back to redirect url provided in step 1 (Also match one of the redirect_uris you registered in the portal).
The client application will get the code and send http post request with code to acquire access token .
So if you want to manally implement the code flow in your application , you can refer to below code sample :
public async Task<IActionResult> Login()
{
string authorizationUrl = string.Format(
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?response_type=code&client_id={1}&redirect_uri={2}&scope={3}",
"tenantID", "ClientID", "https://localhost:44360/Home/CatchCode",
"openid offline_access https://graph.microsoft.com/user.read");
return Redirect(authorizationUrl);
}
private static readonly HttpClient client = new HttpClient();
public async Task<ActionResult> CatchCode(string code)
{
var values = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", "XXXXXX"},
{ "code", code},
{ "redirect_uri", "https://localhost:44360/Home/CatchCode"},
{ "scope", "https://graph.microsoft.com/user.read"},
{ "client_secret", "XXXXXXXXXXX"},
};
var content = new FormUrlEncodedContent(values);
//POST the object to the specified URI
var response = await client.PostAsync("https://login.microsoftonline.com/cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac/oauth2/v2.0/token", content);
//Read back the answer from server
var responseString = await response.Content.ReadAsStringAsync();
//you can deserialize an Object use Json.NET to get tokens
}
That just is simple code sample which will get Microsoft Graph's access token , you still need to care about url encode and catch exception , but it shows how code flow works .

Azure Mobile Services LoginAsync method not working with Microsoft Auth Token

I have successfully been able to get an access_token (or authenticationToken for Microsoft tokens) using the client side authentication in my Xamarin forms App. I am able to get further user information (email, name, etc.) using the same access token. Now, when I try to pass that token to my Azure Mobile Service backend, I get a 401 error.
Here is my code:
private async System.Threading.Tasks.Task<string> MSGetUserInfo(Account account)
{
// Reference: http://graph.microsoft.io/en-us/docs/overview/call_api
// Note that Microsoft don't recognize the access_token header entry, but rely instead on an Authorization header entry
var client = new HttpClient();
var userInfoRequest = new HttpRequestMessage()
{
RequestUri = new Uri("https://graph.microsoft.com/v1.0/me"),
Method = HttpMethod.Get,
};
// Add acccess Bearer
userInfoRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", account.Properties["access_token"]);
using (var response = await client.SendAsync(userInfoRequest).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
Models.User user = new Models.User();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var jobject = JObject.Parse(responseString);
var userName = (string)jobject["userPrincipalName"];
// Check username is valid
if (String.IsNullOrEmpty(userName))
{
throw new Exception("Username was not set for authenticated user");
}
else
user.ProviderLoginId = userName;
var userDisplayName = (string)jobject["displayName"];
// Replace display name if invalid
if (String.IsNullOrWhiteSpace(userDisplayName))
{
userDisplayName = userName;
}
else
user.Name = userDisplayName;
var userEmail = (string)jobject["mail"];
// Replace email if invalid
if (String.IsNullOrWhiteSpace(userEmail))
{
userEmail = userName;
}
else
user.Email = userEmail;
Valufy.App.currentUser = user;
}
else
{
throw new Exception("OAuth2 request failed: " + await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
return "success";
}
The above code snippet works in getting my user details. Now when I try to use the same token in the subsequent call, I get a 404:
public async Task<bool> Authenticate(string token)
{
string message = string.Empty;
var success = false;
JObject objToken = new JObject();
//objToken.Add("access_token", token); //for facebook and google
objToken.Add("authenticationToken", token); //for microsoft
try
{
// Sign in with Facebook login using a server-managed flow.
if (user == null)
{
//ProviderAuth("MICROSOFT");
user = await syncMgr.CurrentClient
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, objToken);
if (user != null)
{
success = true;
message = string.Format("You are now signed-in as {0}.", user.UserId);
}
}
}
catch (Exception ex)
{
message = string.Format("Authentication Failed: {0}", ex.Message);
}
// Display the success or failure message.
// await new MessageDialog(message, "Sign-in result").ShowAsync();
return success;
}
Is there something that I am doing wrong? Any and all assistance is appreciated.
According to your description, I followed this Git sample about Microsoft Graph Connect Sample for UWP (REST). I could get the access_token and it could work as expected with Microsoft Graph API (e.g. Get a user). But when I use this access_token as the authenticationToken token object for MobileServiceClient.LoginAsync, I could also get 401 Unauthorized.
Then I checked the managed client for Azure Mobile Apps about Authenticate users. For Client-managed authentication flow, I found that the official code sample about using Microsoft Account is working with Live SDK as follows:
// Request the authentication token from the Live authentication service.
// The wl.basic scope should always be requested. Other scopes can be added
LiveLoginResult result = await liveIdClient.LoginAsync(new string[] { "wl.basic" });
if (result.Status == LiveConnectSessionStatus.Connected)
{
session = result.Session;
// Get information about the logged-in user.
LiveConnectClient client = new LiveConnectClient(session);
LiveOperationResult meResult = await client.GetAsync("me");
// Use the Microsoft account auth token to sign in to App Service.
MobileServiceUser loginResult = await App.MobileService
.LoginWithMicrosoftAccountAsync(result.Session.AuthenticationToken);
}
Note: As LiveConnectSession states about AuthenticationToken:
The authentication token for a signed-in and connected user.
While check the authentication with Microsoft Graph, I could only find the access_token instead of AuthenticationToken.
UPDATE:
I have checked LiveLogin for WP8 and Microsoft Account Authentication for Mobile Apps via Fiddler to capture the authorize requests. I found that MS account authentication has the similar authorize request as Live SDK.
I assumed that you need to leverage Live SDK to authenticate the user when using client side authentication with Microsoft account. I found the Live SDK download page is not exist, you could follow the Live SDK for WP8 to get started with Live SDK.
UPDATE2:
For the client-flow authentication (Microsoft Account), you could leverage MobileServiceClient.LoginWithMicrosoftAccountAsync("{Live-SDK-session-authentication-token}"), also you could use LoginAsync with the token parameter of the value {"access_token":"{the_access_token}"} or {"authenticationToken":"{Live-SDK-session-authentication-token}"}. I have tested LoginAsync with the access_token from MSA and retrieve the logged info as follows:

Access email address in the OAuth ExternalLoginCallback from Facebook v2.4 API in ASP.NET MVC 5 [duplicate]

This question already has answers here:
Why new fb api 2.4 returns null email on MVC 5 with Identity and oauth 2?
(6 answers)
Closed 6 years ago.
With v2.3 of the Facebook API, provided the following was set, the users email address would be returned on the callback to ExternalLoginCallback;
app.UseFacebookAuthentication(new FacebookAuthenticationOptions
{
AppId = "XXX",
AppSecret = "XXX",
Scope = { "email" }
});
However, any app that can only target v2.4 (released 8 July) no longer returns the email address to the ExternalLoginCallback.
I think this may possibly be related to the v2.4 changes as listed here;
Declarative Fields
To try to improve performance on mobile networks,
Nodes and Edges in v2.4 requires that you explicitly request the
field(s) you need for your GET requests. For example, GET
/v2.4/me/feed no longer includes likes and comments by default, but
GET /v2.4/me/feed?fields=comments,likes will return the data. For more
details see the docs on how to request specific fields.
How can I access this email address now?
To resolve this I had to install the Facebook SDK for .NET from nuget and query the email address separately.
In the ExternalLoginCallback method, I added a conditional to populate the email address from the Facebook Graph API;
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// added the following lines
if (loginInfo.Login.LoginProvider == "Facebook")
{
var identity = AuthenticationManager.GetExternalIdentity(DefaultAuthenticationTypes.ExternalCookie);
var access_token = identity.FindFirstValue("FacebookAccessToken");
var fb = new FacebookClient(access_token);
dynamic myInfo = fb.Get("/me?fields=email"); // specify the email field
loginInfo.Email = myInfo.email;
}
And to get the FacebookAccessToken I extended ConfigureAuth;
app.UseFacebookAuthentication(new FacebookAuthenticationOptions
{
AppId = "XXX",
AppSecret = "XXX",
Scope = { "email" },
Provider = new FacebookAuthenticationProvider
{
OnAuthenticated = context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
return Task.FromResult(true);
}
}
});
In MVC 6 (Asp.net Core 1.0), by configuring FacebookAuthentication in startup.cs like this:
app.UseFacebookAuthentication(options =>
{
options.AppId = Configuration["Authentication:Facebook:AppId"];
options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
options.Scope.Add("email");
options.UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email,first_name,last_name";
});
I could get the email. I.e:
var info = await _signInManager.GetExternalLoginInfoAsync();
var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email);

Thinktecture Identity server v3 - Facebook Assertion Flow

Is there a possibility to configure OAuth2 AssertionFlow with Facebook in Thinktecture Identity Server v3?
There was a post on leastprivilege.com about implementing AssertionFlow for Microsoft OAuth and AuthorizationServer but I need to integrate with Facebook and, furthermore, AuthorizationServer is marked as deprecated and it's not maintained anymore.
In response to #NathanAldenSr's comment, I publish some code of my working solution.
Server side - custom validator:
public class FacebookCustomGrantValidator: ICustomGrantValidator
{
private readonly IUserService userService;
private const string _FACEBOOK_PROVIDER_NAME = "facebook";
// ...
async Task<CustomGrantValidationResult> ICustomGrantValidator.ValidateAsync(ValidatedTokenRequest request)
{
// check assetion type (you can have more than one in your app)
if (request.GrantType != "assertion_fb")
return await Task.FromResult<CustomGrantValidationResult>(null);
// I assume that fb access token has been sent as a response form value (with 'assertion' key)
var fbAccessToken = request.Raw.Get("assertion");
if (string.IsNullOrWhiteSpace(assertion))
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult
{
ErrorMessage = "Missing assertion."
});
AuthenticateResult authebticationResult = null;
// if fb access token is invalid you won't be able to create Facebook client
var client = new Facebook.FacebookClient(fbAccessToken);
dynamic response = client.Get("me", new { fields = "email, first_name, last_name" });
// create idsrv identity for the user
authebticationResult = await userService.AuthenticateExternalAsync(new ExternalIdentity()
{
Provider = _FACEBOOK_PROVIDER_NAME,
ProviderId = response.id,
Claims = new List<Claim>
{
new Claim("Email", response.email),
new Claim("FirstName", response.first_name),
new Claim("LastName", response.last_name)
// ... and so on...
}
},
new SignInMessage());
return new CustomGrantValidationResult
{
Principal = authebticationResult.User
};
}
}
You can easily test it with OAuth2Client that is also provided by Thinktecture (in Thinktexture.IdentityModel Client Library nuget package).
string fbAccessToken = "facebook_access_token_you_aquired_while_logging_in";
string assertionType = "assertion_fb";
var client = new OAuth2Client(
new Uri("your_auth_server_url"),
"idsrv_client_id",
"idsrv_client_secret");
string idsrvAccessToken = client.RequestAssertionAsync(assetionType, fbAccessToken,).Result;
IdentityServer v3 also supports assertion flow. The samples wiki has two samples on that (called "Custom Grants):
https://github.com/thinktecture/Thinktecture.IdentityServer.v3.Samples/tree/master/source

Resources