Get a user's profile using AspNet.Security.OpenIdConnect.Server - oauth-2.0

recently I've spent some time digging into AspNet.Security.OpenIdConnect.Server. I use MVC sample as a code base for my client/server apps.
The thing I need to implement now is obtaining a user's profile info from an external provider (Google) and saving the info into the server's database.
What is the right place for getting and saving a profile's info and a proper way to implement it?

Note: since ASOS (AspNet.Security.OpenIdConnect.Server) only handles the OAuth2/OpenID Connect server part, it's actually not involved in the authentication part, and thus doesn't directly deal with the external providers you configure.
To achieve what you want, the best approach is to configure the Google middleware to extract more information from the user object returned in the user profile response and to persist them in the authentication cookie so you can later retrieve them in your application code.
app.UseGoogleAuthentication(options => {
options.ClientId = "client_id";
options.ClientSecret = "client_secret";
options.Events = new OAuthEvents {
OnCreatingTicket = context => {
// Extract the "language" property from the JSON payload returned by
// the user profile endpoint and add a new "urn:language" claim.
var language = context.User.Value<string>("language");
context.Identity.AddClaim(new Claim("urn:language", language));
return Task.FromResult(0);
}
};
});
If the response doesn't include the data you need, nothing prevents you from using context.Backchannel to make another HTTP call to retrieve more data from a different Google endpoint.

Related

Error when querying Microsoft Graph API Shifts: "MS-APP-ACTS-AS header needs to be set for application context requests"

We are trying to query shifts in the Microsoft Graph API using a C# app, now that StaffHub got deprecated , in the past we were getting an Unknown Error which looked like a permissions issue.
In the docs I noticed permissions for Schedule.ReadAll and Schedule.ReadWriteAll so I added them to the application permissions in our App Registration in Azure.
Now when we send the request to https://graph.microsoft.com/beta/teams/{teamid}/schedule we get this error:
Microsoft.Graph.ServiceException: 'Code: Forbidden Message: {"error":{"code":"Forbidden","message":"MS-APP-ACTS-AS header needs to be set for application context requests.","details":[],"innererror":{"code":"MissingUserIdHeaderInAppContext"}}}
The documentation says the Schedule permissions are in private preview, are these required for querying a schedule & shifts, and if so, is it possible to request access to the private preview?
I'm in the same situation. It's possible to request private preview access (we have), but I'm guessing that it's primarily granted to Microsoft partners or at least have a connection at Microsoft.
The workaround for me has been getting access on behalf of a user. It does however require the user to enter username and password in order to get an access token, so it might not be a perfect solution for you. But it works. You need to add (and, I believe, grant admin consent for) delegated permissions for this to work, either Group.Read.All or Group.ReadWrite.All.
Edit:
I've got it working now. We have private preview access, so I'm not sure this will help you unless you do too, but as I understand it will be available eventually. Given your question, I presume you already have an access token.
Add MS-APP-ACT-AS as a header with the user ID of the user you want the Graph client to act as.
If you're using the Graph SDK for .NET Core you can just add a header to the authentication provider:
public IAuthenticationProvider GetAuthenticationProviderForActingAsUser(string userId, string accessToken)
{
return new DelegateAuthenticationProvider(
requestMessage =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
requestMessage.Headers.Add("MS-APP-ACTS-AS", userId);
return Task.CompletedTask;
});
}
Then you call the graph service client:
var authenticationProvider = GetAuthenticationProviderForActingAsUser(userId, accessToken);
var graphClient = new GraphServiceClient(authenticationProvider);
You should then be able to fetch the shifts:
var shifts = await graphClient.Teams[teamId].Schedule.Shifts
.Request()
.AddAsync(shift);

Call Graph API from MVC App

SUMMARY UPDATE:
I got a sample working today thanks to the many good replies. Thanks all. My primary goal was to get current user information (ME) without using secret key. First I just used the secret key from the App Reg and this will authenticate the App and not the user. This does of course not work when calling ME. My next finding was if you want the users token, you still need the App Reg token, and then you request the users token. This requires less permissions on the App Reg, but requires to request two tokens. I ended up skipping ME and just requesting information for a specified user (through the APp Reg permissions):
$"https://graph.microsoft.com/v1.0/users/{email}/$select=companyName"
Both both approaches should be viable. I updated code below with working sample.
I am trying to do a very simple call to graph API to get companyName from current user. Found some samples but they seemed to be very complicated. The MVC app is authenticated trough an Application Registration in AAD.
I guess the application registration needs to be authorized to access Graph API. Or is more needed here? Getting company name should be fairly simple:
https://graph.microsoft.com/v1.0/me?$select=companyName
Does anyone have a snippet for calling the graph API, my best bet would be you need to extract a bearer token from the controller? ALl help is appreciated.
Working snippet:
public async Task<ActionResult> Index()
{
string clientId = "xxx";
string clientSecret = "xxx";
var email = User.Identity.Name;
AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/xxx.onmicrosoft.com/oauth2/token");
ClientCredential creds = new ClientCredential(clientId, clientSecret);
AuthenticationResult authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
HttpClient http = new HttpClient();
string url = $"https://graph.microsoft.com/v1.0/users/{email}/$select=companyName";
//url = "https://graph.windows.net/xxx.onmicrosoft.com/users?api-version=1.6";
// Append the access token for the Graph API to the Authorization header of the request by using the Bearer scheme.
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
HttpResponseMessage response = await http.SendAsync(request);
var json = response.Content.ReadAsStringAsync();
return View();
}
To add one last item, here is a link to an MVC sample on Git that uses an MVC application to send email. It illustrates how to call the MS Graph API to get various pieces of information. Keep in mind, if you are using an application only scenario, ME will not work, the sample illustrates how to obtain a delegated token for a user and use that toke to do work:
https://github.com/microsoftgraph/aspnet-connect-rest-sample
If I am reading this code snippet correctly, You are requesting a application only token for the Graph.Microsoft.Com resource, then attempting to use that toke with this URI:
url = "https://graph.windows.net/thomaseg.onmicrosoft.com/users?api-version=1.6"
This will not work because you are mixing resources, AAD Graph and MS Graph. The ME endpoint does not make since in this scenario because you are using the application only flow. This flow does not support the ME endpoint. ME is designed for use with a delegated token. the ME endpoint represents the signed in user, since and application is not a user, ME is meaningless.
You will need to target the user specifically:
https://graph.microsoft.com/v1.0/Users/[UPN or ID of user]?$select=companyName
Should work if your application has been granted the appropriate permission scopes.

How do you use an iOS Google ID Token to fetch a user's full profile from the backend (Node JS)?

I'm using the Google Login iOS SDK to login, then passing GIDGoogleUser.authentication.idToken to the server, which I'm then verifying in Node JS. The verification in the code below works fine. "payload" var ends up being correct with basic information about the user.
How do I translate the idToken into credentials that I can use to git the people.get endpoint? (I want to know whether the user is using the default Google profile photo or not, and that is available from the people.get endpoint.) This does not seem to be documented anywhere.
https://developers.google.com/people/api/rest/v1/people/get
var auth = new GoogleAuth;
var client = new auth.OAuth2(GoogleUtils.clientIDs, '', '');
client.verifyIdToken(
token,
GoogleUtils.clientIDs,
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3],
function(e, login) {
if (e) {
return next(e, null);
}
var payload = login.getPayload();
return next(null, payload);
});
Thanks for your help. I can't seem to find any of this info in the documentation. Google's APIs are very poorly documented it seems.
Unfortunately, as noted, the current ID token payload does not say whether the photo is the default one (probably something we should add). If you need an access token to call Google's REST APIs (such as people.get) for more user data, then you need to obtain an OAuth auth code, and exchange it for access and refresh tokens, as documented at https://developers.google.com/identity/sign-in/ios/offline-access

How to get the value of access token in ASP.NET Core MVC OAuth 2.0

In Visual Studio 2017RC I created ASP.NET Core MVC app with individual user accounts and successfully completed https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins tutorial to attach Google authentication. I'm now logged in via my Google account.
All I did was adding a few lines to the autogenerated code (in Configure method of Startup.cs):
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = "xxxx.apps.googleusercontent.com",
ClientSecret = "xxxx",
Scope = { "email", "openid" }
});
I now need to get the value of access token which was issued by Google (and stored in cookies by the app). I'll then use it to generate XOAuth2 key to access Google services. For instance, in HomeController's About method (auto-generated by the standard wizard) I want to display the number of unread emails in my inbox. With XOAuth2 key, I can log in my Gmail and proceed from here.
How can I get this token?
- Do I need to store access token in database during initial logging in via Google? If so, any clues how this can be done in the standard wizard-generated ASP.NET Core MVC app?
- Or, maybe I can always read the access token from cookies? If so, how?
Preferably, I'd read it from cookies (it's anyway there) and avoid duplicating this info in database but not sure if this approach is feasible (i.e. if it can be decrypted).
I did this for ASP.NET MVC once but in ASP.NET Core MVC things have changed a lot, the legacy code is of no use anymore.
OK, found it. SaveTokens property does the trick.
app.UseGoogleAuthentication(new GoogleOptions
{
ClientId = "xxxx.apps.googleusercontent.com",
ClientSecret = "xxxx",
Scope = { "email", "openid" },
SaveTokens = true,
AccessType = "offline"
});
I can then get access token in AccountController.ExternalLoginCallback
var token = info.AuthenticationTokens.Single(x => x.Name == "access_token").Value;

persist bot chat data in bot framework wrt to Form Flow

I want to know whether is there any feature in bot framework to get the user and bot chat completely. I have gone through the official documentation, but the way I understood it is that, only to that context we can save the chat data. If at all we have to store the whole data, we have to take care of it.
I tried using this,
StateClient sc = activity.GetStateClient();
BotData userData1 =
sc.BotState.GetConversationData(activity.ChannelId, activity.Conversation.Id);
userData1.Data = userData1.Data + activity.Text;
sc.BotState.SetConversationData(activity.ChannelId, activity.Conversation.Id, userData1);
This does persist the user data, but I am stuck with how to persist it in the form flow.
I am not sure how to persist data of bot and user wrt to Form Flow using SetConversationData. I even need the bot to persist the prompt message of the form flow. So that I ll have the complete conversation b/w user and the bot.
This tutorial may help:
Introduction To FormFlow With The Microsoft Bot Framework
The data from the user is automatically persisted during the FormFlow.
When the FormFlow is completed you can persist it like this:
public static IForm<ProfileForm> BuildForm()
{
return new FormBuilder<ProfileForm>()
.Message("Welcome to the profile bot!")
.OnCompletion(async (context, profileForm) =>
{
// Set BotUserData
context.PrivateConversationData.SetValue<bool>(
"ProfileComplete", true);
context.PrivateConversationData.SetValue<string>(
"FirstName", profileForm.FirstName);
context.PrivateConversationData.SetValue<string>(
"LastName", profileForm.LastName);
context.PrivateConversationData.SetValue<string>(
"Gender", profileForm.Gender.ToString());
// Tell the user that the form is complete
await context.PostAsync("Your profile is complete.");
})
.Build();
}

Resources