Getting the email from external providers Google and Facebook during account association step in a default MVC5 app - asp.net-mvc-5

Apparently you can do this with the Facebook provider by adding scopes to the FacebookAuthenticationOptions object in Startup.Auth.cs:
http://blogs.msdn.com/b/webdev/archive/2013/10/16/get-more-information-from-social-providers-used-in-the-vs-2013-project-templates.aspx
List<string> scope = new List<string>() { "email" };
var x = new FacebookAuthenticationOptions();
x.Scope.Add("email");
...
app.UseFacebookAuthentication(x);
How to do the same with Google provider? There isn't a x.Scope property for the GoogleAuthenticationOptions class/object!

PLEASE SEE UPDATES AT THE BOTTOM OF THIS POST!
The following works for me for Facebook:
StartupAuth.cs:
var facebookAuthenticationOptions = new FacebookAuthenticationOptions()
{
AppId = "x",
AppSecret = "y"
};
facebookAuthenticationOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookAuthenticationOptions);
ExternalLoginCallback method:
var externalIdentity = HttpContext.GetOwinContext().Authentication.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
var email = emailClaim.Value;
And for Google:
StartupAuth.cs
app.UseGoogleAuthentication();
ExternalLoginCallback method (same as for facebook):
var externalIdentity = HttpContext.GetOwinContext().Authentication.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
var email = emailClaim.Value;
If I set a breakpoint here:
var email = emailClaim.Value;
I see the email address for both Facebook and Google in the debugger.
Update 1: The old answer had me confused so I updated it with the code I have in my own project that I just debugged and I know works.
Update 2: With the new ASP.NET Identity 2.0 RTM version you no longer need any of the code in this post. The proper way to get the email is by simply doing the following:
Startup.Auth.cs
app.UseFacebookAuthentication(
appId: "x",
appSecret: "y");
app.UseGoogleAuthentication();
AccountController.cs
//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
var result = await SignInHelper.ExternalSignIn(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresTwoFactorAuthentication:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}

You need to explicitly configure the FacebookAuthenticationOptions to get the email address from the authenticated user.
In your MVC5 project, add these lines in the Startup.Auth.cs
var options = new FacebookAuthenticationOptions() {
AppId = "xxxxxxxx",
AppSecret = "xxxxxxxxx"
};
options.Scope.Add("email");
app.UseFacebookAuthentication(options);
Update
Reduced my sample code to the absolute minimum. Your updated code works fine by the way, I have also tried it with both Facebook and Google.

In ASP.NET Core Facebook authentication the Facebook middleware seems to no longer pass in the email, even if you add it to the scope. You can work around it by using Facebook's Graph Api to request the email.
You can use any Facebook Graph Api client or roll your own, and use it to invoke the Graph api as follows:
app.UseFacebookAuthentication(options =>
{
options.AppId = Configuration["Authentication:Facebook:AppId"];
options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
options.Scope.Add("public_profile");
options.Scope.Add("email");
options.Events = new OAuthEvents
{
OnCreatingTicket = context => {
// Use the Facebook Graph Api to get the user's email address
// and add it to the email claim
var client = new FacebookClient(context.AccessToken);
dynamic info = client.Get("me", new { fields = "name,id,email" });
context.Identity.AddClaim(new Claim(ClaimTypes.Email, info.email));
return Task.FromResult(0);
}
};
});
You can find a more detailed example about how to use it here: http://zainrizvi.io/2016/03/24/create-site-with-facebook-login-using-asp.net-core/#getting-the-email-address-from-facebook

Related

node-ews returning 401 Unauthorized where as using the valid access token

I am using node-ews to fetch emails from the Microsoft Exchange server.
It was working fine with basic auth.
But, as Microsoft disabled basic auth.
We are currently using the OAuth token (access token) from Graph Explorer to test.
But it's returning 401 Unauthorised error.
This is the sample code we are using to connect to the exchange server.
const ewsConfig = {
username: item.mail_username,
password: item.user_pass,
host: item.ews_host,
token: 'xxxxxxxxxxx',
auth: 'bearer'
};
// initialize node-ews
const options = {
rejectUnauthorized: false,
strictSSL: false
};
// initialize node-ews
const ews = new EWS(ewsConfig, options);
. We are currently using the OAuth token (access token) from Graph Explorer to test.
The Graph Explorer token won't have permissions for EWS only Graph, the only two permission that are valid in EWS are EWS.AccessAsUser.All or full_access_as_app if using the client credentials flow. https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth the Mail.Read etc permission don't work in EWS because it doesn't support the more restrictive authentication scheme that Graph supports (which is a reason to use the Graph over EWS)
If you want to accesstoken to test with use the EWSEditor https://github.com/dseph/EwsEditor/releases and grab its token
Part 1-1 - Setup application in AZURE that allows to generate MSAL-access token for EWS:
Login to MS AZURE portal.
Open "App registration" tool:
step2_img
Click "New Registration":
step3_img
Setup new App:
step4_img
After you click registrate button you will receive smtg like this:
step5_img
Open API permissions tab for previously created App + click Add permission and select MS Graph:
step6_img
Select Delegated permissions:
step7_img
Find User section and select User.Read + Add permission click:
step8_img
Add a permission again + APIs my organizaton uses tab(or find it) and find Office 365 Exchange Online:
step9_img
Part-1-2 - continue...
Part 2 - get accessToken by using userName + userPassword to email box:
import * as path from 'path';
import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';
public async getEmailAccessToken(
clientId: string,
tenantId: string,
emailUserName: string,
emailUserPassword: string,
cacheFilePath: string = `.${path.sep}tokenCache.json`) {
const msal = require('#azure/msal-node');
const { promises: fs } = require('fs');
//Cache Plugin configuration
const beforeCacheAccess = async (cacheContext) => {
try {
const cacheFile = await fs.readFile(cacheFilePath, 'utf-8');
cacheContext.tokenCache.deserialize(cacheFile);
} catch (error) {
// if cache file doesn't exists, create it
cacheContext.tokenCache.deserialize(await fs.writeFile(cacheFilePath, ''));
}
};
const afterCacheAccess = async (cacheContext) => {
if (cacheContext.cacheHasChanged) {
try {
await fs.writeFile(cacheFilePath, cacheContext.tokenCache.serialize());
} catch (error) {
console.log(error);
}
}
};
const cachePlugin = {
beforeCacheAccess,
afterCacheAccess
};
const msalConfig = {
auth: {
clientId: clientId, // YOUR clientId
authority: `https://login.microsoftonline.com/${tenantId}` // YOUR tenantId
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose
}
}
};
const pca = new msal.PublicClientApplication(msalConfig);
const msalTokenCache = pca.getTokenCache();
const accounts = await msalTokenCache.getAllAccounts();
// Acquire Token Silently if an account is present
let accessToken = null;
if (accounts.length > 0) {
const silentRequest = {
account: accounts[0], // Index must match the account that is trying to acquire token silently
scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
};
const response = await pca.acquireTokenSilent(silentRequest);
accessToken = response.accessToken;
} else {
// fall back to username password if there is no account
const usernamePasswordRequest = {
scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
username: emailUserName, // Add your username here
password: emailUserPassword, // Add your password here
};
const response = await pca.acquireTokenByUsernamePassword(usernamePasswordRequest);
accessToken = response.accessToken;
}
return accessToken;
}
This method returns accessToken allows us to use EWS-api and also generates tokenCacheFile.json that will be used for silent usage in case of multiple calls.
Part 3 - connect to emailbox by using previously generated accessToken and ews-javascript-api :
import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';
public async connectAndChangeAllEmailsFromBlaBla(
clientId: string,
tenantId: string,
exchangeServiceUrl: string = 'https://outlook.office365.com/Ews/Exchange.asmx',
emailUserName: string,
emailUserPassword: string,
searchMask: string = 'hasattachments:yes and from:NoReply#blabla.com and received:today') {
// get acces token by method written above in part 2
const emailAccessToken = await this.getEmailAccessToken(clientId, tenantId, emailUserName, emailUserPassword);
const ews = require('ews-javascript-api');
const service = new ExchangeService(ews.ExchangeVersion.Exchange2013);
// use emailAccesToken
service.Credentials = new OAuthCredentials(emailAccessToken);
service.Url = new ews.Uri(exchangeServiceUrl);
const mailInbox = await ews.Folder.Bind(service, ews.WellKnownFolderName.Inbox);
const loadPageSize = 1000; // 1 means load last email according to filter
const view = new ews.ItemView(loadPageSize);
view.PropertySet = new ews.PropertySet(ews.BasePropertySet.FirstClassProperties);
let mailItems;
// hasattachment:yes
// isread:false
// received:today or received:[date]
mailItems = await mailInbox.FindItems(searchMask, view);
console.log(`Emails were found before processing: ${mailItems.Items.length}`);
for (const item of mailItems.Items) {
// mark mail.item as read
item.IsRead = true;
await item.Update(1);
// Do what you want
}
return mailItems.Items.length;
}
Part 0 - Please find the solution we used to fix the same problem.
The solution consist of 3 parts:
Setup application in AZURE that allows to generate MSAL-access token for EWS.
Add code to get accessToken.
Made changes in old code to use previously received accessToken. I am usind ews-javascript-api. But I think previouse two steps will help you to get accessToken for EWS and you can use it with node-EWS.
Sorry for 3 posts, but as a new user I have a restrictions it impossible for new users to create posts with more than 8 links and etc... )
Part 1-2 - continue:
Find EWS section and select EWS.AccessAsUser.All and click Add permissons:
step10_img
Go to Authentication tab and click Add platform:
step11_img
Select Mobile and Desctop apps and click Save button:
step12_img
Select two options and click Configure:
step13-1_img
step13-2_img
Also on Authentication tab set "Supported accounts types" and "Allow public client flows" and click Save:
step14_img
Go to Overview tab you should see smthg like this:
clientID
tenantId
step15_img
THIS STEP should be made BY EACH USER that WILL USE this API - use USER credentials to open this link (or YOUR ADMIN should make bulk apply). Check made changes by opening next link in browser in incognito mode(FOR each user):
https://login.microsoftonline.com/ADD YOUR TENANTID/oauth2/v2.0/authorize?
client_id=ADD YOUR CLIENTID
&response_type=code
&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
&response_mode=query
&scope=EWS.AccessAsUser.All
&state=12345
After the opening previously generated link you should login and then receive another link in browser which shoud contains generated code:
step16_img
Now we can start add code allows us to get accessToken

How do I create a user in code, add it Azure AD B2C, then log in as that user using Azure AD B2C OAuth (MSAL)

Having read the documentation here I thought I should be able to add a user to active directory B2C and then be able to log in as that user. The error message is: "We can't seem to find your account"
[TestMethod]
public async Task CreateUserTest()
{
string mailNickname = Guid.NewGuid().ToString();
string upn = mailNickname + "#mydomain.onmicrosoft.com";
string email = "zzz#gmail.com";
User record = new User { Email = email, DisplayName = "Bob Smith", MailNickname = mailNickname, UserPrincipalName = upn };
record.Identities = new List<ObjectIdentity>();
record.PasswordProfile = new PasswordProfile();
record.Identities.Append(new ObjectIdentity { Issuer = "mydomain.onmicrosoft.com", IssuerAssignedId = email, ODataType = "microsoft.graph.objectidentity", SignInType = "emailAddress" });
record.Identities.Append(new ObjectIdentity { Issuer = "mydomain.onmicrosoft.com", IssuerAssignedId = upn, ODataType = "microsoft.graph.objectidentity", SignInType = "userPrincipalName" });
record.PasswordProfile.Password = "Abcdefgh123!!";
record.AccountEnabled = true;
record.PasswordProfile.ForceChangePasswordNextSignIn = false;
User user = await graphService.CreateUser(record);
Assert.IsNotNull(user);
}
public async Task<User> CreateUser(User user)
{
var result = await client.Users.Request().AddAsync(user);
return user;
}
This login code works if the user logs in using an existing account or creates a new one using the Sign up now link:
export const SignIn = async (appState: AppState): Promise<string> => {
var msg: string = '';
try {
const response = await MSAL.login('loginPopup');
Edit: Add screen cap showing user type and source:
I tried to create a consumer user with code like yours:
And tested with this account in user flow, it returned the token well:
Please check the accounts that you created in your code, the User type always need to be Member and have the Source Azure Active Directory.

Why do users have different ID formats between the Microsoft Graph API and UserIDs in bot framework?

I have used Graph Service to get user information by email. Here is my sample code:
var user = null;
const GraphService = require('graph-service');
const ClientCredentials = require('client-credentials');
const tenant = 'my-company.com';
const clientId = '0b13aa29-ca6b-42e8-a083-89e5bccdf141';
const clientSecret = 'lsl2isRe99Flsj32elwe89234ljhasd8239jsad2sl=';
const credentials = new ClientCredentials(tenant, clientId, clientSecret);
const service = new GraphService(credentials);
service.get('/users/tnguyen482#my-company.com').then(response => {
user = response.data;
});
This would return user which has ID = 9422e847-0000-1111-2222-d39d550a4fb6
But when I use Botbuilder-teams to get fetch members, the user information return from which has different format of ID. Here is my sample code:
var user = null;
var teams = require("botbuilder-teams");
var connector = new teams.TeamsChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
let conversationId = session.message.address.conversation.id;
var userEmail = "tnguyen482#my-company.com";
connector.connector.fetchMembers(
"https://smba.trafficmanager.net/amer-client-ss.msg/",
conversationId,
(err, result) => {
if (err) {
console.log('Cannot get member of current conversation');
}
else {
if (result.length > 0){
result.forEach(function(item) {
if (item.email == userEmail){
user = item;
}
});
}
}
}
);
This would return user which has ID = 29:1zJXjlM7ifjqawGVxXx_4xxx56BFCCIJWfPbWrVDSdxsKUhi9IXyXXYNLOKCLHodN7WgEzz31lBKcZwtWvMzoUw
My question is why on the same user with different ways approach to retrieve data return different ID format?
Besides, my purpose is that I will use the user ID in address for botbuilder to send personal message to user.
User ID is not defined the same within the Graphs Service as it is within Botbuilder. The botbuilder userID is a key for that given user as connected to the conversation within the bot (and is only relevant within the context of the conversation with the bot), and the userID within Graph Service is a unique identity key for a user of Azure AD.
These are not the same API or part of a universal connector, so these IDs do not cross over to one another. Many people create some sort of dictionary of users so that the 2 can be looked up and used accordingly in their application.

How to get back to app after google login

I'm trying to implement google login in my app using xamarin.auth like below
var auth = new OAuth2Authenticator("284202576320-7kgdhaa5sgvkoe03jmmcv0p8lfdma306.apps.googleusercontent.com","cAZW7uegD-h2-
tNMMf5q1UGQ","https://www.googleapis.com/auth/userinfo.email",new
Uri("https://accounts.google.com/o/oauth2/auth"),new
Uri("http://dev.myfav.restaurant/Account/LoginComplete"),new
Uri("https://accounts.google.com/o/oauth2/token"),null,true)
{
AllowCancel = true,
};
but Completed event not firing and its going to web page after login :(
I'm getting below error
i need to get back user to my app how can i achieve this ???? Can anyone help me on this please.
Thanks in advance
Hey follow these two examples one is using web view and one is using google sign in sdk for google auth.
https://timothelariviere.com/2017/09/01/authenticate-users-through-google-with-xamarin-auth/
and
https://developer.xamarin.com/samples/xamarin-forms/WebServices/OAuthNativeFlow/
So according to this issue reported by Mounika.Kola .I think u should refer that authenticator.Completed -= OnAuthCompleted is there in ur code. For reference just see these codes which i used for google authorization in Xamarin Forms.
void OnLoginClicked(object sender, EventArgs e)
{
string clientId = null;
string redirectUri = null;
switch (Device.RuntimePlatform)
{
case Device.iOS:
clientId = Constants.iOSClientId;
redirectUri = Constants.iOSRedirectUrl;
break;
case Device.Android:
clientId = Constants.AndroidClientId;
redirectUri = Constants.AndroidRedirectUrl;
break;
}
var authenticator = new OAuth2Authenticator(
clientId,
null,
Constants.Scope,
new Uri(Constants.AuthorizeUrl),
new Uri(redirectUri),
new Uri(Constants.AccessTokenUrl),
null,
true);
authenticator.Completed += OnAuthCompleted;
authenticator.Error += OnAuthError;
AuthenticationState.Authenticator = authenticator;
var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
presenter.Login(authenticator);
}
async void OnAuthCompleted(object sender, AuthenticatorCompletedEventArgs e)
{
var authenticator = sender as OAuth2Authenticator;
if (authenticator != null)
{
authenticator.Completed -= OnAuthCompleted;
authenticator.Error -= OnAuthError;
}
User user = null;
if (e.IsAuthenticated)
{
// If the user is authenticated, request their basic user data from Google
// UserInfoUrl = https://www.googleapis.com/oauth2/v2/userinfo
var request = new OAuth2Request("GET", new Uri(Constants.UserInfoUrl), null, e.Account);
var response = await request.GetResponseAsync();
if (response != null)
{
// Deserialize the data and store it in the account store
// The users email address will be used to identify data in SimpleDB
string userJson = await response.GetResponseTextAsync();
user = JsonConvert.DeserializeObject<User>(userJson);
}
if (account != null)
{
store.Delete(account, Constants.AppName);
}
await store.SaveAsync(account = e.Account, Constants.AppName);
await DisplayAlert("Email address", user.Email, "OK");
}
}
I hope it helps you.
In iOS once you have completed the authentication with Xamarin.Auth you just need to dismiss the viewController and you will be put back in your app.
You do this subscribing to the Completed event of the OAuth2Authenticator
auth.Completed += (sender, e) =>
{
DismissViewController(true, null);
};
If the "Native UI" is used (the last parameter in the constructor is set to true), which means that external/system browser is used for login not WebView. So, on Android instead of WebView [Chrome] CustomTabs is used and on iOS instead of UIWebView (or WKWebView) SFSafariViewController is used.
With native UI user is leaving your app and the only way to return to your app is app-linking (or deep-linking) and this requires completely different approach.
1st you cannot use http[s] scheme for redirect_url (OK on Android it is possible, but on iOS not). Use custom scheme for that.
See the sample[s] (Xamarin.Forms ComicBook):
https://github.com/moljac/Xamarin.Auth.Samples.NugetReferences
And the docs in the repo:
https://github.com/xamarin/Xamarin.Auth/tree/master/docs

How to get and display the email using the Facebook provider in ASP.NET Identity and MVC 5

UPDATE: PLEASE SEE THIS POST: https://stackoverflow.com/a/20379623/687549
Been reading I think almost all questions on SO about external providers and how to get extra info/data/metadata/claims/orwhateveryouwannacallit in particular the email address which many use as the username on modern websites.
So the problem that I was facing was that I wanted to retrieve the email from the Facebook provider with as little code as possible. I thought to myself; the new ASP.NET Identity framework must have this buil-in and can do this with probably just a couple of lines of code. I searched and all I could find was these imensely large chunks of code and I thought to myself: there has got to be another more simpler way. And here it is, as an answer in this questionthread.
I managed to get this working with both Facebook and Google but what I'm concerned about is wheather or not I'm doing this right without any consequenses somewhere else in the code.
For instance: Do you really only need to specify the Scopes.Add("email") for it all to work or why haven't I been able to find more info about this on the interweb?
UPDATE: PLEASE SEE THIS POST: https://stackoverflow.com/a/20379623/687549
Startup.Auth.cs:
var facebookAuthenticationOptions = new FacebookAuthenticationOptions()
{
AppId = "myAppId",
AppSecret = "myAppSecret"
};
facebookAuthenticationOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookAuthenticationOptions);
AccountController (default mvc 5 template app stuff)
//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// These next three lines is how I get the email from the stuff that gets returned from the Facebook external provider
var externalIdentity = HttpContext.GetOwinContext().Authentication.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
var email = emailClaim.Value;
// Sign in the user with this external login provider if the user already has a login
var user = await UserManager.FindAsync(loginInfo.Login);
if (user != null)
{
await SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
// Populate the viewmodel with the email
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { UserName = email });
}
}
I have the same problem. You need to edit and add this code to ExternalLoginCallback in the AccountController
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;
}
Note the code dynamic myInfo = fb.Get("/me?fields=email"); this will work for facebook app with version 2.4, but for old version you can write this
dynamic myInfo = fb.Get("email");

Resources