Microsoft Identity Platform in Node-Red without ROPC - oauth-2.0

Is there a way to use Microsoft Identiy Platform with a OAuth 2.0 flow without using ROPC auth flow in Node-Red? I can't use ROPC, because the destination tenant enforces MFA. ROPC will be blocked when MFA is enforced.
I found the plugin node-red-contrib-oauth2, but wasn't able to get this working with Microsoft Identity Platform with another OAuth 2.0 flow, other than ROPC.

The solution is to use Device Code Flow. The following instructions give you a flow which is able to read your Microsoft Teams presence status / Microsoft Office 365 presence status with node-red.
Creating the App in the Azure Portal
In the following example we will create an app which is able to read only the logged in users presence. This means, that the API permissions may vary depending on your needs.
Login to the Azure Portal and go to Azure Active Directory, then to "App Registartions" and click "+ New registration"
Type a friendly name that helps identifying the app.
Supported Accounts: Accounts in this organizational directory only (* - Single tenant)
Redirect URI: leave empty, as we will set this later.
Click "Register"
Go to "API Permissions".
Remove the default permission as it is not needed for the presence.
Click "+ Add a permission".
Click "Microsoft Graph", then "delegated permissions".
Select "offline_access" and "Presence.Read" and save it with "Add permissions"
Explanation:
Presence.Read is needed by the Graph API to access only the resource owner's presence (not to be not to be confused with Presence.Read.All).
offline_access allows to retrieve a refresh token
Then you have to "admin consent these permissions" by clicking the "Grant admin consent for *"-button next to the "+ Add permission button".
Navigate back to "Overview" and copy the values of "Application (client) ID", "Directory (tenant) ID".
Select "Authentication" in the navigation drawer.
Click "+ Add Platform"
Select "Mobile and desktop applications"
Select "https://login.microsoftonline.com/common/oauth2/nativeclient"
Submit with "Configure"
Scroll down to "Advanced Settings" and select "Yes" next to "Treat application as a public client." and submit with "Save" in the upper left corner.
Now this app can be used in Node-Red for reading the presence from MS Graph API.
Use this App in Node-Red
A good starting point for a flow of that kind is this:
[{"id":"7c76e545.92af9c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"40792ca0.843c24","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":710,"y":160,"wires":[["44c485e1.fa8d2c","643ed329.5bdfec"]]},{"id":"419648fb.f1a818","type":"inject","z":"7c76e545.92af9c","name":"launch device code request","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":160,"wires":[["427185e9.4132fc"]]},{"id":"657e48c9.bcda48","type":"function","z":"7c76e545.92af9c","name":"Set refresh_token","func":"flow.get('refresh_token', function(err, refresh_token) {\n if (err) {\n node.error(err, msg);\n } else {\n // initialise the counter to 0 if it doesn't exist already\n refresh_token = msg.payload.refresh_token;\n // store the value back\n flow.set('refresh_token',refresh_token, function(err) {\n if (err) {\n node.error(err, msg);\n } else {\n // make it part of the outgoing msg object\n msg.refresh_token = refresh_token;\n // send the message\n node.status({fill:\"green\",shape:\"dot\",text:`refresh_token: ${msg.refresh_token}`});\n node.send(msg);\n }\n });\n }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":990,"y":340,"wires":[[]]},{"id":"d1253feb.c9362","type":"function","z":"7c76e545.92af9c","name":"Set access_token","func":"flow.get('access_token', function(err, access_token) {\n if (err) {\n node.error(err, msg);\n } else {\n // initialise the counter to 0 if it doesn't exist already\n access_token = msg.payload.access_token;\n // store the value back\n flow.set('access_token',access_token, function(err) {\n if (err) {\n node.error(err, msg);\n } else {\n // make it part of the outgoing msg object\n msg.access_token = access_token;\n // send the message\n node.status({fill:\"green\",shape:\"dot\",text:`access_token: ${msg.access_token}`});\n node.send(msg);\n }\n });\n }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":990,"y":300,"wires":[[]]},{"id":"44c485e1.fa8d2c","type":"function","z":"7c76e545.92af9c","name":"Set device_code","func":"flow.get('device_code', function(err, refresh_token) {\n if (err) {\n node.error(err, msg);\n } else {\n // initialise the counter to 0 if it doesn't exist already\n device_code = msg.payload.device_code;\n // store the value back\n flow.set('device_code',device_code, function(err) {\n if (err) {\n node.error(err, msg);\n } else {\n // make it part of the outgoing msg object\n msg.device_code = device_code;\n // send the message\n node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${msg.device_code}`});\n node.send(msg);\n }\n });\n }\n});\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":950,"y":160,"wires":[[]]},{"id":"648d5fe3.74d0b","type":"function","z":"7c76e545.92af9c","name":"","func":"var context = flow.get(['tenant_id','client_id','scope','device_code']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope = context[2];\nvar device_code = context[3];\n\nif(!device_code)\n{\n msg.delay = 5*1000;\n return [msg, null];\n}\n\nif(tenant_id && client_id && scope && device_code)\n{\n msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\";\n msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n msg.payload = {\n \"client_id\": client_id,\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n \"scope\": scope,\n \"code\": device_code\n }\n node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${device_code.substring(0, 10)}`});\n return [null, msg];\n}\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":320,"y":320,"wires":[["ddaa0b36.e42108"],["1fc0a330.6fe22d"]]},{"id":"ec8a6999.9abcd8","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":320,"wires":[["648d5fe3.74d0b"]]},{"id":"1fc0a330.6fe22d","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":320,"wires":[["d32c769a.1c42b8"]]},{"id":"ba3c9093.320a7","type":"comment","z":"7c76e545.92af9c","name":"Retrieve tokens ...","info":"... after login has been made in a browser","x":130,"y":260,"wires":[]},{"id":"d4b1dae2.c6c538","type":"comment","z":"7c76e545.92af9c","name":"refresh tokens every 30 minutes","info":"","x":170,"y":400,"wires":[]},{"id":"396bdeb0.93db72","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1800","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":440,"wires":[["8739bad6.405738"]]},{"id":"8739bad6.405738","type":"function","z":"7c76e545.92af9c","name":"refresh request","func":"var context = flow.get(['tenant_id','client_id','scope','refresh_token']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope = context[2];\nvar refresh_token = context[3];\n\nmsg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\"; \nmsg.headers = {\n \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n \"grant_type\": \"refresh_token\",\n \"client_id\": client_id,\n \"refresh_token\": `${refresh_token}`,\n \"scope\": scope\n\n};\n\nif(tenant_id && client_id && scope && refresh_token )\n return msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":440,"wires":[["1aafbb4a.e050b5"]]},{"id":"1aafbb4a.e050b5","type":"http request","z":"7c76e545.92af9c","name":"","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":440,"wires":[["d32c769a.1c42b8"]]},{"id":"97a36414.d2b318","type":"http request","z":"7c76e545.92af9c","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://graph.microsoft.com/beta/me/presence","tls":"","persist":false,"proxy":"","authType":"","x":490,"y":580,"wires":[["cd2de8e1.fdbdd8"]]},{"id":"4a82e18f.d26e","type":"inject","z":"7c76e545.92af9c","name":"","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":580,"wires":[["f74a2254.e0487"]]},{"id":"f74a2254.e0487","type":"function","z":"7c76e545.92af9c","name":"","func":"var access_token = flow.get('access_token'); \n\nif(!access_token)\n{\n node.status({fill:\"blue\",shape:\"dot\",text:`Access token missing. Exiting`});\n return null;\n}\n\nmsg.headers= {\n \"Authorization\": \"Bearer \"+access_token\n };\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":320,"y":580,"wires":[["97a36414.d2b318"]]},{"id":"cd2de8e1.fdbdd8","type":"function","z":"7c76e545.92af9c","name":"","func":"var response = msg.payload;\nif(response.hasOwnProperty('availability') && response.hasOwnProperty('activity'))\n{\n node.status({fill:\"green\",shape:\"dot\",text:`Status: ${response.availability} (${response.activity})`});\n return [ { \"payload\": {\n \"availability\": response.availability,\n \"activity\": response.activity\n }}]; \n}\nnode.status({fill:\"red\",shape:\"ring\",text:`Status: some error occurred`});\n\nconsole.log(\"no property availability\");","outputs":1,"noerr":0,"initialize":"","finalize":"","x":700,"y":580,"wires":[[]]},{"id":"8e2a7b69.0d2f38","type":"comment","z":"7c76e545.92af9c","name":"Available Presence Properties","info":"[Docs](https://learn.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties)","x":770,"y":520,"wires":[]},{"id":"b28b4494.18e868","type":"inject","z":"7c76e545.92af9c","name":"change my values","props":[{"p":"scope","v":"Presence.Read offline_access","vt":"str"},{"p":"tenant_id","v":"","vt":"str"},{"p":"client_id","v":"","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payloadType":"str","x":130,"y":40,"wires":[["4bb4827.9197c7c"]]},{"id":"4bb4827.9197c7c","type":"function","z":"7c76e545.92af9c","name":"prepare context","func":"flow.get('tenant_id', function(err, tenant_id) {\n if (err) {\n node.error(err, msg);\n } else {\n // store the value\n flow.set('tenant_id',msg.tenant_id, function(err) {\n if (err) {\n node.error(err, msg);\n } else {\n flow.get('scope', function(err, scope) {\n if (err) {\n node.error(err, msg);\n } else {\n // store the value\n flow.set('scope',msg.scope, function(err) {\n if (err) {\n node.error(err, msg);\n } else {\n flow.get('client_id', function(err, client_id) {\n if (err) {\n node.error(err, msg);\n } else {\n // store the value\n flow.set('client_id',msg.client_id, function(err) {\n if (err) {\n node.error(err, msg);\n } \n // no else here\n });\n }\n });\n }\n });\n }\n });\n node.status({fill:\"green\",shape:\"dot\",text:`OK: context prepared`});\n }\n });\n }\n});","outputs":1,"noerr":0,"initialize":"","finalize":"","x":360,"y":40,"wires":[[]]},{"id":"427185e9.4132fc","type":"function","z":"7c76e545.92af9c","name":"prepare device code request","func":"msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n\nvar context = flow.get(['tenant_id','client_id','scope']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope = context[2];\nif(tenant_id && client_id && scope)\n{\n msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/devicecode\"\n msg.payload = { \n \"client_id\": client_id,\n \"scope\": scope\n };\n node.status({fill:\"green\",shape:\"dot\",text:`Values passed on`});\n return msg;\n}\n\nnode.status({fill:\"red\",shape:\"dot\",text:`ERROR: context not prepared`});","outputs":1,"noerr":0,"initialize":"","finalize":"","x":460,"y":160,"wires":[["40792ca0.843c24"]]},{"id":"ddaa0b36.e42108","type":"delay","z":"7c76e545.92af9c","name":"","pauseType":"delayv","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":520,"y":240,"wires":[["648d5fe3.74d0b"]]},{"id":"d32c769a.1c42b8","type":"function","z":"7c76e545.92af9c","name":"","func":"var context = flow.get(['access_token','refresh_token']);\nvar access_token = context[0]; \nvar refresh_token = context[1]; \n\nif(msg.payload.hasOwnProperty('access_token') && \nmsg.payload.hasOwnProperty('refresh_token'))\n{\n flow.set('device_code',undefined);\n node.status({fill:\"green\",shape:\"dot\",text:`device now logged in, pass on message`});\n return [null, msg]; \n}\n\n\nif(access_token && refresh_token)\n{\n flow.set('device_code',undefined);\n node.status({fill:\"green\",shape:\"dot\",text:`device already logged in`});\n return [];\n}\n\nif(msg.payload.hasOwnProperty('error'))\n{\n if(msg.payload.error == \"authorization_pending\")\n {\n node.status({fill:\"blue\",shape:\"dot\",text:`Browser login pending`});\n msg.delay = 5*1000;\n return [msg, null]; \n }\n node.status({fill:\"red\",shape:\"dot\",text:`Error: ${msg.payload.error_description}`});\n return [];\n}\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":740,"y":320,"wires":[["ddaa0b36.e42108"],["d1253feb.c9362","657e48c9.bcda48"]]},{"id":"643ed329.5bdfec","type":"debug","z":"7c76e545.92af9c","name":"Auth link and device code","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":980,"y":100,"wires":[]},{"id":"2347386d.7321d8","type":"comment","z":"7c76e545.92af9c","name":"MS Graph request (presence)","info":"[Docs](https://learn.microsoft.com/en-us/graph/api/resources/presence?view=graph-rest-beta#properties)","x":190,"y":520,"wires":[]}]
This gives you the following flow which is able to read your Microsoft Teams presence status / Microsoft Office 365 presence status with node-red:
Double click the "change my values"-node and enter client_id, tenant_id and the used scope. The scopes default values for reading the presence are already set.
Deploy the node.
Launch the "launch device code request".
In the debug console you will get a code which you should copy and open the given link in a browser on any device.
Go through the authentication process in the browser until it shows you that the login was successful and you can close that browser window.
Next click the inject node under "retrieve tokens".
If the nodes to the right show green values everything was successful and you are ready to go.

Related

Firebase Authenticated User created without email address - Google from IOS

I have set up firebase authentication and am trying to send a welcome email
What I do not understand is why the authenticated user does not have an email field and only has this in the provider data. All the samples use something similar to:
exports.sendWelcomeEmail = functions.auth.user().onCreate(event => {
const user = event.data; // The Firebase user.
const email = user.email; // The email of the user.
const displayName = user.displayName; // The display name of the user.
console.log("New User created: " + JSON.stringify(user));
console.log('email:', email);
However, my authenticated user does not seem to me to have an email
My log shows
email: undefined
I am using google authentication from IOS with GIDSignInButton
My console log has the following
{
"displayName": "Ryan H",
"metadata": {
"createdAt": "2017-06-19T10:06:21.000Z",
"lastSignedInAt": "2017-06-19T10:06:21.000Z"
},
"photoURL": "https://lh6.googleusercontent.com/-MXne-lIR8e8/AAAAAAAAAAI/AAAAAAAAbeQ/Z1OvxasY/s96-c/photo.jpg",
"providerData": [
{
"displayName": "Ryan H",
"email": "ryanh#gmail.com",
"photoURL": "https://lh6.googleusercontent.com/-MXne-lIR8e8/AAAAAAAAAAI/AAAAAAAAbeQ/Z1asY/s96-c/photo.jpg",
"providerId": "google.com",
"uid": "1077081708"
}
],
"uid": "WjdlLc3QNvrmkj0yOuqo2"
}
As you can see there is no email except in the provider data.
Has there been a change in the model?
Should I always try to get my email address from the provider data?
Why are all the same code and examples I find using user.email ?
I currently have google and facebook enabled as "sign in methods".
Perhaps if I also enabled email/password I would have access.
I can confirm that in my IOS app the User after sign in does not have email either.
According to https://firebase.google.com/docs/auth/users
The first time a user signs up to your app, the user's profile data is populated using the available information:
If the user signed up with an email address and password, only the primary email address property is populated
If the user signed up with a federated identity provider, such as Google or Facebook, the account information made available by the provider is used to populate the Firebase User's profile
This is a workaround to my issue not really an answer:
I extract an email from the first provider I find, which in my case is always
Google
exports.sendWelcomeEmail = functions.auth.user().onCreate(event => {
const user = event.data; // The Firebase user.
var email = user.email; // The email of the user.
if (email == undefined) {
for (var provider of user.providerData) {
if (provider.email) {
email = provider.email;
break;
}
}
}
const displayName = user.displayName; // The display name of the user.
// [END eventAttributes]
console.log("New User created: " + JSON.stringify(user));
console.log('email:', email);
console.log('displayName:', displayName);
sendWelcomeEmail(email,displayName)
return;
});`

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:

Requests to MS Graph API gives me "Authorization Request Denied - Insufficient privileges to complete the operation"

I have a question about "Authorization Request Denied - Insufficient privileges to complete the operation" message that I keep getting back from my requests to Windows Graph API.
Specifically, I'm working in Azure cloud. I have an iOS mobile app that invokes an API.
I have turned on "Authentication for Active Directory" in my Portal.
Then, on the client side (iOS):
[self.todoService.client loginWithProvider:#"windowsazureactivedirectory"
controller:self
animated:YES
completion:^(MSUser *user, NSError *error) {
if(!error && user) {
[self refresh];
}
}]; //loginWithProvider
So returns a valid MSUser object. I see the web login controller appear, I sign in with my un/pw, and then it lets me access my Easy Table's data...etc.
Now, I want to invoke an Easy API that I've created in Azure called getUserData. Hence, I simply insert the invokeAPI code like this (iOS):
[self.todoService.client loginWithProvider:#"windowsazureactivedirectory"
controller:self
animated:YES
completion:^(MSUser *user, NSError *error) {
if(!error && user) {
//NSMutableDictionary * dict = [NSMutableDictionary dictionary];
//[dict setObject:#YES forKey:#"complete"];
NSLog(#"%s - %#", __FUNCTION__, user);
[self refresh];
[self.todoService.client invokeAPI:#"getUserData"
body:nil
HTTPMethod:#"POST"
parameters:nil
headers:nil
completion:^(id _Nullable result, NSHTTPURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(#"%s - API returned response! ", __FUNCTION__);
NSLog(#"%#", result); //TODO: user info here!! :D
}]; //invokAPI
} //if user returned from AAD login is valid
}]; //loginWithProvider
Everything is fine as the API is called and I can see the response data.
On the server side (Node JS), I basically do 3 things:
1st is to get the user object id from the request object:
req.azureMobile.user.getIdentity().then((data) => {
//get user object ID
}
2nd, make a request to https://login.windows.net to get an Access Token with a username/password.
var options = {
url: "https://login.windows.net/" + tenant_domain + "/oauth2/token?api-version=1.0",
method: 'POST',
form: {
grant_type: "client_credentials",
resource: "https://graph.windows.net",
client_id: clientID,
client_secret: key
}
};
req(options, function (err, resp, body) {
//get the result back
}
I get a whole bunch of data back including the Access Token.
3rd, make a request to https://graph.windows.net/, and provide this Access Token along with my User Object ID:
var options = {
url: "https://graph.windows.net/" + tenant_domain + "/users/" + objectId + "?api-version=1.0",
method: 'GET',
headers: {
"Authorization": "Bearer " + access_token
}
};
This is so that I can User data. Now, in a separate test Subscription, I set up all the basic read permissions for AAD and Graph in my AAD management. I successfully get the user's full data back like so:
user = {
accountEnabled = 1;
assignedLicenses = (
);
assignedPlans = (
);
city = xxxxxxxxx;
country = xxxxxxxxxx;
department = Dev;
dirSyncEnabled = "<null>";
displayName = xxxxxx;
facsimileTelephoneNumber = "<null>";
givenName = hehe;
jobTitle = "iOS dev";
lastDirSyncTime = "<null>";
mail = "<null>";
mailNickname = "xxxxxxxxxx.com#EXT#";
mobile = "+xx xxx xxxx 3852";
objectId = "xxxxxxx-2c70-4aab-b261-3b2b97dc5c50";
objectType = User;
"odata.metadata" = "https://graph.windows.net/xxxxxxxxxx.onmicrosoft.com/$metadata#directoryObjects/Microsoft.WindowsAzure.ActiveDirectory.User/#Element";
"odata.type" = "Microsoft.WindowsAzure.ActiveDirectory.User";
otherMails = (
"xxxxxxxxxxxx#gmail.com"
);
...etc
}
However, in another subscription, I did the exact same steps. Even going as far as checking all the permissions like so:
I keep getting an "Authorization Request Denied, Insufficient privileges" message. The error is null so I know everything else went through correctly.
I can't figure out why because everything processes through and I checked all of my AAD and Graph permissions.
log result:
-----body------
'{"odata.error":{"code":"Authorization_RequestDenied","message":{"lang":"en","value":"Insufficient privileges to complete the operation."}}}'
Thanks for any help, and appreciate everyone's time
You can try to upgrade the role of the AD application you use to a administrator permission. Run the following commands in PowerShell:
Connect-MsolService
$ClientIdWebApp = '{your_AD_application_client_id}'
$webApp = Get-MsolServicePrincipal –AppPrincipalId $ClientIdWebApp
#use Add-MsolRoleMember to add it to "Company Administrator" role).
Add-MsolRoleMember -RoleName "Company Administrator" -RoleMemberType ServicePrincipal -RoleMemberObjectId $webApp.ObjectId

Outlook Office 365 : Refresh token failed to retrieve because "AADSTS70000" the provided value for the 'code' parameter is not valid

In below code I can retrieve refresh token successfully from email#company.com email addresses. However, when I try to login with email#outlook.com it doesn't give the refresh token instead it returns this response.
Response:
{
"error": "invalid_grant",
"error_description": "AADSTS70000: The provided value for the 'code' parameter is not valid. The code has expired.\r\nTrace ID: ...\r\nCorrelation ID: ...\r\nTimestamp: 2016-05-19 10:13:05Z",
"error_codes": [
70000
],
"timestamp": "2016-05-19 10:13:05Z",
"trace_id": "8cceb393-....",
"correlation_id": "5227de8...."
}
Code:
private async Task<string> GetRefreshRoken(string authCode, string onSuccessRedirectUri) {
var client = new HttpClient();
var parameters = new Dictionary<string, string>
{
{"client_id", _clientId},
{"client_secret", _clientSecret},
{"code",authCode }, // what retreived from //https://login.microsoftonline.com/common with authroization.
{"redirect_uri", onSuccessRedirectUri}, //http://localhost:27592/Home/Authorize
{"grant_type","authorization_code" }
};
var content = new FormUrlEncodedContent(parameters);
var response = await client.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", content);
var tokensJsonString = await response.Content.ReadAsStringAsync();
dynamic token = Newtonsoft.Json.JsonConvert.DeserializeObject(tokensJsonString);
return token.refresh_token;
}
So I had googled with the error number and found http://www.matvelloso.com/2015/01/30/troubleshooting-common-azure-active-directory-errors/ page where the error describes:
Then I had changed my redirecting url to "http://localhost:27592/Home/Authorize/". Since I am using this https://dev.outlook.com/restapi/tutorial/dotnet tutorial as a reference , now I cannot login with any other account.
Is there any good approach to retrieve refresh tokens for outlook account?
For windows live id account, you will get error "The provided value for the 'code' parameter is not valid. The code has expired." when using the authorization code twice.
The correct way to refresh the token is using refresh token (v2.0 token reference > Refresh Token).
First, ensure you have declare the scope "offline_access".
Then, you will get the access_token when acquire the token using grant_type=code (the first time you acquire the token).
Next, you need to use grant_type=refresh_token to refresh your access token.

How can I login to Meteor with native device Facebook?

Suppose I logged into my device's Facebook authentication, like system Facebook on iOS. I obtain an access token.
How can I use the access token to login to Meteor's Facebook Oauth provider?
To login with Facebook using an access token obtained by another means, like iOS Facebook SDK, define a method on the server that calls the appropriate Accounts method:
$FB = function () {
if (Meteor.isClient) {
throw new Meteor.Error(500, "Cannot run on client.");
}
var args = Array.prototype.slice.call(arguments);
if (args.length === 0) {
return;
}
var path = args[0];
var i = 1;
// Concatenate strings together in args
while (_.isString(args[i])) {
path = path + "/" + args[i];
i++;
}
if (_.isUndefined(path)) {
throw new Meteor.Error(500, 'No Facebook API path provided.');
}
var FB = Meteor.npmRequire('fb');
var fbResponse = Meteor.sync(function (done) {
FB.napi.apply(FB, [path].concat(args.splice(i)).concat([done]));
});
if (fbResponse.error !== null) {
console.error(fbResponse.error.stack);
throw new Meteor.Error(500, "Facebook API error.", {error: fbResponse.error, request: args});
}
return fbResponse.result;
};
Meteor.methods({
/**
* Login to Meteor with a Facebook access token
* #param accessToken Your Facebook access token
* #returns {*}
*/
facebookLoginWithAccessToken: function (accessToken) {
check(accessToken, String);
var serviceData = {
accessToken: accessToken
};
// Confirm that your accessToken is you
try {
var tokenInfo = $FB('debug_token', {
input_token: accessToken,
access_token: Meteor.settings.facebook.appId + '|' + Meteor.settings.facebook.secret
});
} catch (e) {
throw new Meteor.Error(500, 'Facebook login failed. An API error occurred.');
}
if (!tokenInfo.data.is_valid) {
throw new Meteor.Error(503, 'This access token is not valid.');
}
if (tokenInfo.data.app_id !== Meteor.settings.facebook.appId) {
throw new Meteor.Error(503, 'This token is not for this app.');
}
// Force the user id to be the access token's user id
serviceData.id = tokenInfo.data.user_id;
// Returns a token you can use to login
var loginResult = Accounts.updateOrCreateUserFromExternalService('facebook', serviceData, {});
// Login the user
this.setUserId(loginResult.userId);
// Return the token and the user id
return loginResult;
}
}
This code depends on the meteorhacks:npm package. You should call meteor add meteorhacks:npm and have a package.json file with the Facebook node API: { "fb": "0.7.0" }.
If you use demeteorizer to deploy your app, you will have to edit the output package.json and set the scrumptious dependency from "0.0.1" to "0.0.0".
On the client, call the method with the appropriate parameters, and you're logged in!
In Meteor 0.8+, the result of Accounts.updateOrCreateUserFromExternalService has changed to an object containing {userId: ...} and furthermore, no longer has the stamped token.
You can get the accessToken in the Meteor.user() data at Meteor.user().services.facebook.accessToken (be aware this can only be accessed on the server side as the services field is not exposed to the client.
So when a user logs in with facebook on your meteor site these fields would be populated with the user's facebook data. If you check your meteor user's database with mongo or some other gui tool you could see all the fields which you have access to.
Building on DrPangloss' most excellent answer above, combining it with this awesome post: http://meteorhacks.com/extending-meteor-accounts.html
You'll run into some issues using ObjectiveDDP in trying to get the client persist the login. Include the header:
#import "MeteorClient+Private.h"
And manually set the required internals. Soon I'll make a meteorite package and an extension to MyMeteor (https://github.com/premosystems/MyMeteor) but for now it's manual.
loginRequest: {"accessToken":"XXXXXb3Qh6sBADEKeEkzWL2ItDon4bMl5B8WLHZCb3qfL11NR4HKo4TXZAgfXcySav5Y8mavDqZAhZCZCnDDzVbdNmaBAlVZAGENayvuyStkTYHQ554fLadKNz32Dym4wbILisPNLZBjDyZAlfSSgksZCsQFxGPlovaiOjrAFXwBYGFFZAMypT9D4qcZC6kdGH2Xb9V1yHm4h6ugXXXXXX","fbData":{"link":"https://www.facebook.com/app_scoped_user_id/10152179306019999/","id":"10152179306019999","first_name":"users' first name","name":"user's Full Name","gender":"male","last_name":"user's last name","email":"users#email.com","locale":"en_US","timezone":-5,"updated_time":"2014-01-11T23:41:29+0000","verified":true}}
Meteor.startup(
function(){
Accounts.registerLoginHandler(function(loginRequest) {
//there are multiple login handlers in meteor.
//a login request go through all these handlers to find it's login hander
//so in our login handler, we only consider login requests which has admin field
console.log('loginRequest: ' + JSON.stringify(loginRequest));
if(loginRequest.fbData == undefined) {
return undefined;
}
//our authentication logic :)
if(loginRequest.accessToken == undefined) {
return null;
} else {
// TODO: Verfiy that the token from facebook is valid...
// https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.0#checktoken
// graph.facebook.com/debug_token? input_token={token-to-inspect}&access_token={app-token-or-admin-token}
}
//we create a user if not exists, and get the userId
var email = loginRequest.fbData.email || "-" + id + "#facebook.com";
var serviceData = {
id: loginRequest.fbData.id,
accessToken: loginRequest.accessToken,
email: email
};
var options = {
profile: {
name: loginRequest.fbData.name
}
};
var user = Accounts.updateOrCreateUserFromExternalService('facebook', serviceData, options);
console.log('Logged in from facebook: ' + user.userId);
//send loggedin user's user id
return {
userId: user.userId
}
});
}
);
This answer could be improved further as we can now directly debug the token from a REST http request using futures. Credit still goes to #DoctorPangloss for the principal steps necessary.
//Roughly like this - I removed it from a try/catch
var future = new Future();
var serviceData = {
accessToken: accessToken,
email: email
};
var input = Meteor.settings.private.facebook.id + '|' + Meteor.settings.private.facebook.secret
var url = "https://graph.facebook.com/debug_token?input_token=" + accessToken + "&access_token=" + input
HTTP.call( 'GET', url, function( error, response ) {
if (error) {
future.throw(new Meteor.Error(503, 'A error validating your login has occured.'));
}
var info = response.data.data
if (!info.is_valid) {
future.throw(new Meteor.Error(503, 'This access token is not valid.'));
}
if (info.app_id !== Meteor.settings.private.facebook.id) {
future.throw(new Meteor.Error(503, 'This token is not for this app.'));
}
// Force the user id to be the access token's user id
serviceData.id = info.user_id;
// Returns a token you can use to login
var user = Accounts.updateOrCreateUserFromExternalService('facebook', serviceData, {});
if(!user.userId){
future.throw(new Meteor.Error(500, "Failed to create user"));
}
//Add email & user details if necessary
Meteor.users.update(user.userId, { $set : { fname : fname, lname : lname }})
Accounts.addEmail(user.userId, email)
//Generate your own access token!
var token = Accounts._generateStampedLoginToken()
Accounts._insertLoginToken(user.userId, token);
// Return the token and the user id
future.return({
'x-user-id' : user.userId,
'x-auth-token' : token.token
})
});
return future.wait();
Use this instead of the JS lib suggested by #DoctorPangloss. Follow the same principles he suggested but this avoids the need to integrate an additional library

Resources