I am trying to use the Unified API (Microsoft.Graph 1.0.1) to access my users profil photos, but I only get the following error back when accessing the photo:
Code: ErrorAccessDenied
Message: Access is denied. Check credentials and try again.
Accessing/Listing the other user profile data works fine and my application was added as a "Company Administrator" via PowerShell and has all rights set in the management portal. When I use the GraphExlorer logged in with my admin user it also works fine. Also via the "old" Azure Active Directory Graph API I can read/write to the users thumbnail photo, but thats not the one showing up in Office 365.
How can I get the appropriate access rights to perform actions on users profile photo?
This is the code I use (shortened to the parts in question):
class Program
{
private const string authStringMicrosoft = "https://login.microsoftonline.com/MY_APP_ID/";
private const string clientID = "MY_CLIENT_ID";
private const string clientSecret = "MY_CLIENT_SECRET";
private const string graphResourceId = "https://graph.microsoft.com";
static void Main(string[] args)
{
AsyncContext.Run(RunAsync);
Console.WriteLine("DONE");
Console.ReadLine();
}
private static async Task RunAsync()
{
var token = await GetAppTokenAsync(authStringMicrosoft, graphResourceId);
var authHelper = new AuthenticationHelper() { AccessToken = token }
var graphClient = new GraphServiceClient(authHelper);
await ListUser(graphClient);
}
private static async Task ListUser(GraphServiceClient graphClient)
{
Console.WriteLine("User-List:");
var users = await graphClient.Users.Request().GetAsync();
foreach (var user in users)
{
Console.WriteLine($"{user.UserPrincipalName}:\t\t{user.GivenName} {user.Surname}");
if (user.UserPrincipalName == "USER_WITH_PICTURE")
{
var graphUser = graphClient.Users[user.UserPrincipalName];
var graphPhoto = graphUser.Photo;
var photoInfo = await graphPhoto.Request().GetAsync(); // <= here the exceptions is thrown
Console.WriteLine($"{photoInfo.Id}:\t{photoInfo.Width}x{photoInfo.Height}");
var photoStream = await graphPhoto.Content.Request().GetAsync();
byte[] photoByte = new byte[photoStream.Length];
photoStream.Read(photoByte, 0, (int)photoStream.Length);
File.WriteAllBytes(#"D:\User.jpg", photoByte);
}
}
}
private static async Task<string> GetAppTokenAsync(string authority, string azureGraphAPI)
{
var authenticationContext = new AuthenticationContext(authority);
var clientCred = new ClientCredential(clientID, clientSecret);
var authenticationResult = await authenticationContext.AcquireTokenAsync(azureGraphAPI, clientCred);
return authenticationResult.AccessToken;
}
}
public class AuthenticationHelper : IAuthenticationProvider
{
public string AccessToken { get; set; }
public Task AuthenticateRequestAsync(HttpRequestMessage request)
{
request.Headers.Add("Authorization", "Bearer " + AccessToken);
return Task.FromResult(0);
}
}
I use the following NuGet-packages:
<packages>
<package id="Microsoft.Data.Edm" version="5.7.0" targetFramework="net46" />
<package id="Microsoft.Data.OData" version="5.7.0" targetFramework="net46" />
<package id="Microsoft.Data.Services.Client" version="5.7.0" targetFramework="net46" />
<package id="Microsoft.Graph" version="1.0.1" targetFramework="net46" />
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="2.24.304111323" targetFramework="net46" />
<package id="Newtonsoft.Json" version="8.0.3" targetFramework="net46" />
<package id="Nito.AsyncEx" version="3.0.1" targetFramework="net46" />
<package id="System.Spatial" version="5.7.0" targetFramework="net46" />
</packages>
This is an example request delivering the error (using postman with the token read out from the app above):
GET /v1.0/users/MY_USER_WITH_PHOTO/photo/ HTTP/1.1
Host: graph.microsoft.com
Connection: keep-alive
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1...
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
Postman-Token: e756a8a3-22e2-d40c-8e52-15c4d1aa7468
Accept: /
Accept-Encoding: gzip, deflate, sdch
Accept-Language: de,en-US;q=0.8,en;q=0.6
And the response:
HTTP/1.1 403 Forbidden
Cache-Control: private
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-IIS/8.5
request-id: 96e8dda8-2353-4891-8c42-99cfe7e22887
client-request-id: 96e8dda8-2353-4891-8c42-99cfe7e22887
x-ms-ags-diagnostic: {"ServerInfo":{"DataCenter":"North Europe","Slice":"SliceA","ScaleUnit":"001","Host":"AGSFE_IN_4","ADSiteName":"DUB"}}
Duration: 1367.7691
X-Powered-By: ASP.NET
Date: Sun, 01 May 2016 17:57:02 GMT
Body:
{
"error": {
"code": "ErrorAccessDenied",
"message": "Access is denied. Check credentials and try again.",
"innerError": {
"request-id": "96e8dda8-2353-4891-8c42-99cfe7e22887",
"date": "2016-05-01T17:57:02"
}
}
}
Again, if I remove the /photo from the request I get all common user details without a problem.
Here the permissions of my app (web-app):
Here a decrypted access token:
{
typ: "JWT",
alg: "RS256",
x5t: "MnC_VZcATfM5pOYiJHMba9goEKY",
kid: "MnC_VZcATfM5pOYiJHMba9goEKY"
}.
{
aud: "https://graph.microsoft.com",
iss: "https://sts.windows.net/11205e59-fa81-480f-b497-571579c5389a/",
iat: 1462795409,
nbf: 1462795409,
exp: 1462799309,
appid: "c34a87ef-352a-4af4-a166-eb7e521a0ec9",
appidacr: "1",
idp: "https://sts.windows.net/11205e59-fa81-480f-b497-571579c5389a/",
oid: "1db8c6b5-10ba-40ac-bbff-86ab440c4fd3",
roles: [
"Mail.ReadWrite",
"Device.ReadWrite.All",
"User.ReadWrite.All",
"Calendars.Read",
"Group.Read.All",
"Directory.ReadWrite.All",
"Contacts.ReadWrite",
"Group.ReadWrite.All",
"Directory.Read.All",
"User.Read.All",
"Mail.Read",
"Calendars.ReadWrite",
"Mail.Send",
"MailboxSettings.ReadWrite",
"Contacts.Read"
],
sub: "1db8c6b5-10ba-40ac-bbff-86ab440c4fd3",
tid: "11205e59-fa81-480f-b497-571579c5389a",
ver: "1.0"
}
Just in case anyone else reads this when getting that error. I had this same error when creating my own graphClient and the reason I got it was due to using a non-admin account with...
var users = await graphClient.Users.Request().Select().GetAsync();
With a non-admin account, you only have access to some basic properties like lastname, firstname etc - this worked for me...
var users = await graphClient.Users.Request().Select("mail,givenName,surname").GetAsync();
Related
I used to try this with GCM but I couldn't get it to work with Parse Server .. So I took a stackoverflow users advice and gave it a try with FCM .
My device gets the registration id from FCM like this :
04-15 17:01:29.773 I/parse.GcmRegistrar(30144): GCM registration successful. Registration Id: APA91bFoNUPYdsjN6O_CkPje-O0hXjNz9kvURZMex72xClyBr_5o6D0vYtI-F0iyAGgSYjpIEaJt2QQ2CXk2qpI11gPFUSUdzH-NxQRXSK3hPkuaiC_lciVV3E0fp6A_VZUoYJ8VxOIh
I tried to send a notification from the firebase console with this ID and its working my event gets fired and its all good .
The problem starts when i want to use ParseCloud function to send notifications to my users. While I was searching the device output log for errors i found this one :
04-15 17:01:25.490 E/parse.GcmRegistrar(30144): Found com.parse.push.gcm_sender_id <meta-data> element with value "id:767075137222", but the value is missing the expected "id:" prefix
This one is weird cause my manifest includes the gcm_sender_id plus it includes the prefix id: Here is my manifest
<?xml version="1.0" encoding="utf-8"?>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:name="com.companyname.appname.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.companyname.appname.permission.C2D_MESSAGE" />
<application android:label="Fuse.Android" android:icon="#mipmap/ic_launcher">
<service android:name="parse.ParsePushService" />
<receiver android:name="parse.ParsePushBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="com.companyname.appname" />
</intent-filter>
</receiver>
<meta-data android:name="com.parse.push.gcm_sender_id"
android:value="id:767075137222"/>
</application>
I searched online and people were saying that this problem comes when you're not using the right API KEY & Sender Id .. I'm using these :
Next my index for Parse Server looks like this :
// Example express application adding the parse-server module to expose Parse
// compatible API routes.
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var path = require('path');
var databaseUri = process.env.DATABASE_URI || process.env.MONGODB_URI;
if (!databaseUri) {
console.log('DATABASE_URI not specified, falling back to localhost.');
}
var pushConfig = {};
if (process.env.GCM_SENDER_ID && process.env.GCM_API_KEY) {
pushConfig['android'] = {
senderId: process.env.GCM_SENDER_ID || '',
apiKey: process.env.GCM_API_KEY || ''};
}
var api = new ParseServer({
databaseURI: databaseUri || 'mongodb://localhost:27017/dev',
cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',
appId: process.env.APP_ID || 'myAppId',
masterKey: process.env.MASTER_KEY || '', //Add your master key here. Keep it secret!
serverURL: process.env.SERVER_URL || 'http://localhost:1337/parse', // Don't forget to change to https if needed
push: pushConfig,
liveQuery: {
classNames: ["Posts", "Comments"] // List of classes to support for query subscriptions
}
});
// Client-keys like the javascript key or the .NET key are not necessary with parse-server
// If you wish you require them, you can set them as options in the initialization above:
// javascriptKey, restAPIKey, dotNetKey, clientKey
var app = express();
// Serve static assets from the /public folder
app.use('/public', express.static(path.join(__dirname, '/public')));
// Serve the Parse API on the /parse URL prefix
var mountPath = process.env.PARSE_MOUNT || '/parse';
app.use(mountPath, api);
// Parse Server plays nicely with the rest of your web routes
app.get('/', function(req, res) {
res.status(200).send('I dream of being a website. Please star the parse-server repo on GitHub!');
});
// There will be a test page available on the /test path of your server url
// Remove this before launching your app
app.get('/test', function(req, res) {
res.sendFile(path.join(__dirname, '/public/test.html'));
});
var port = process.env.PORT || 1337;
var httpServer = require('http').createServer(app);
httpServer.listen(port, function() {
console.log('parse-server-example running on port ' + port + '.');
});
// This will enable the Live Query real-time server
ParseServer.createLiveQueryServer(httpServer);
I've defined the GCM_SENDER_ID and GCM_API_KEY to the config vars of the Heroku hosting Parse .
After i call the ParseCloud function from the client app i get this in the heroku logs :
Apr 15 08:03:02 fuseparse app/web.1: } method=POST, url=/parse/push, host=fuseparse.herokuapp.com, connection=close, user-agent=node-XMLHttpRequest, Parse/js1.11.1 (NodeJS 9.11.1), accept=*/*, content-type=text/plain, x-request-id=ea046fd0-5fb7-46b7-9ceb-e6a0fd2ebad1, x-forwarded-for=54.81.77.161, x-forwarded-proto=https, x-forwarded-port=443, via=1.1 vegur, connect-time=0, x-request-start=1523804582292, total-route-time=0, content-length=270, installationId=e2dc9f85-3c2f-464e-beca-c8b9d2cba528, alert=The Giants scored!
Apr 15 08:03:02 fuseparse app/web.1: verbose: RESPONSE from [POST] /parse/push: {
Apr 15 08:03:02 fuseparse app/web.1: "headers": {
Apr 15 08:03:02 fuseparse app/web.1: "X-Parse-Push-Status-Id": "upnMh1652U"
Apr 15 08:03:02 fuseparse app/web.1: },
Apr 15 08:03:02 fuseparse app/web.1: "response": {
Apr 15 08:03:02 fuseparse app/web.1: "result": true
Apr 15 08:03:02 fuseparse app/web.1: }
Apr 15 08:03:02 fuseparse app/web.1: } X-Parse-Push-Status-Id=upnMh1652U, result=true
Apr 15 08:03:02 fuseparse app/web.1: #### PUSH OK
Apr 15 08:03:02 fuseparse app/web.1: verbose: _PushStatus upnMh1652U: sending push to installations with 1 batches
Apr 15 08:03:02 fuseparse app/web.1: verbose: Sending push to 1
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM sending to 1 device
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM GCM Response: {
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "multicast_id": 5516369214301735000,
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "success": 0,
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "failure": 1,
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "canonical_ids": 0,
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "results": [
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM {
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM "error": "MismatchSenderId"
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM }
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM ]
Apr 15 08:03:02 fuseparse app/web.1: node-pre-gyp verb parse-server-push-adapter GCM }
Apr 15 08:03:02 fuseparse app/web.1: verbose: _PushStatus upnMh1652U: sent push! 0 success, 1 failures
Apr 15 08:03:02 fuseparse app/web.1: verbose: _PushStatus upnMh1652U: needs cleanup devicesToRemove=[]
I've been at it for days .. Can somebody tell me if what im trying to do is possible and if it is possible where i might be doing this wrong ???
if you are using fcm notification you need to add this in your manifest !
<!-- Firebase Notifications -->
<service android:name=".HERE_YOUR_CLASS_WHICH_EXTENDS_FirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:name=".HERE_YOUR_CLASS_WHICH_EXTENDS_FirebaseInstanceIDService">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<!-- ./Firebase Notifications -->
Whoever has a problem with this error :
04-15 17:01:25.490 E/parse.GcmRegistrar(30144): Found com.parse.push.gcm_sender_id <meta-data> element with value "id:767075137222", but the value is missing the expected "id:" prefix
but has defined the gcm_sender_id in the androidmanifest most likely has a faulty sdk or parse.dll installed . What i did is I downloaded the open source SDK which is available in the parseplatform github and modified the GcmRegistrar.cs class to not return null for the sender_id :
This took me a long time but for anyone who has the same problem pls download the latest .dll or if the problem still persists it means that the dll was not updated and you have to manually do this . In my case i was using the .NET Sdk and it wasn't updated .
Download the open source sdk and replace Internal/Push/GcmRegistrar.cs with this
using System;
using Android.App;
using Android.Content;
using Android.OS;
using System.Threading.Tasks;
namespace Parse {
internal class GcmRegistrar {
private const string LogTag = "parse.GcmRegistrar";
private const string ExtraRegistrationId = "registration_id";
private const string ExtraSenderId = "com.parse.push.gcm_sender_id";
private const string ParseGcmSenderId = "1076345567071";
public const string IntentRegisterAction = "com.google.android.c2dm.intent.REGISTER";
private readonly Object mutex = new Object();
private Request request;
private Context context;
public static GcmRegistrar GetInstance() {
return Singleton.Instance;
}
private static class Singleton {
public static readonly GcmRegistrar Instance = new GcmRegistrar(Application.Context);
}
private GcmRegistrar(Context context) {
this.context = context;
}
private string getActualSenderIdFromExtra(Object senderIdExtra) {
if (senderIdExtra == null ) {
return null;
}
string senderId = senderIdExtra.ToString();
if (!senderId.StartsWith("id:")) {
return null;
}
return senderId.Substring(3);
}
public void Register() {
ParseInstallation installation = ParseInstallation.CurrentInstallation;
lock (mutex) {
if (installation.DeviceToken == null && request == null) {
var metadata = ManifestInfo.GetApplicationMetaData();
object senderIdExtra = null;
if (metadata != null) {
senderIdExtra = metadata.Get(ExtraSenderId);
}
string senderIds = ParseGcmSenderId;
if (senderIdExtra != null) {
string senderId = getActualSenderIdFromExtra(senderIdExtra);
if (senderId != null) {
senderIds += "," + senderId;
} else {
Android.Util.Log.Error("parse.GcmRegistrar", "Found " + ExtraSenderId + " <meta-data> element with value \""
+ senderIdExtra.ToString() + "\", but the value is missing the expected \"id:\" prefix");
}
}
request = Request.CreateAndSend(this.context, senderIds);
}
}
}
/// <summary>
/// Handles GCM registration intent from <see cref="ParsePushBroadcastReceiver"/> and saves the GCM registration
/// id as <see cref="ParseInstallation.CurrentInstallation"/> device token.
/// </summary>
/// <remarks>
/// Should be called by a broadcast receiver or service to handle GCM registration response
/// intent (com.google.android.c2dm.intent.REGISTRATION).
/// </remarks>
/// <param name="intent"></param>
public Task HandleRegistrationIntentAsync(Intent intent) {
if (intent.Action == ParsePushBroadcastReceiver.ActionGcmRegisterResponse) {
string registrationId = intent.GetStringExtra(ExtraRegistrationId);
if (registrationId != null && registrationId.Length > 0) {
Android.Util.Log.Info(LogTag, "GCM registration successful. Registration Id: " + registrationId);
ParseInstallation installation = ParseInstallation.CurrentInstallation;
// Set `pushType` via internal `Set` method since we want to skip mutability check.
installation.Set("pushType", "gcm");
installation.DeviceToken = registrationId;
return installation.SaveAsync();
}
}
return Task.FromResult(0);
}
/// <summary>
/// Encapsulates the GCM registration request-response, potentially using <c>AlarmManager</c> to
/// schedule retries if the GCM service is not available.
/// </summary>
private class Request {
private Context context;
private string senderId;
private PendingIntent appIntent;
public static Request CreateAndSend(Context context, string senderId) {
Request request = new Request(context, senderId);
request.Send();
return request;
}
private Request(Context context, string senderId) {
this.context = context;
this.senderId = senderId;
appIntent = PendingIntent.GetBroadcast(context, 0, new Intent(), 0);
}
private void Send() {
Intent intent = new Intent(IntentRegisterAction);
intent.SetPackage("com.google.android.gsf");
intent.PutExtra("sender", senderId);
intent.PutExtra("app", appIntent);
ComponentName name = null;
try {
name = context.StartService(intent);
} catch (Exception) {
// Do nothing.
}
}
}
}
}
We have been succesful in using the odata v8.1 endpoint in 2016 to impersonate a user.
Please note that the intended request flow is: Postman-->LocalHost Microservice-->CRM
Example of a working request from Postman-->CRM (directly, without going through the microservice)
Accept:application/json
Content-Type:application/json; charset=utf-8
OData-MaxVersion:4.0
OData-Version:4.0
MSCRMCallerID:d994d6ff-5531-e711-9422-00155dc0d345
Cache-Control:no-cache
Against the odata endpoint: ..../api/data/v8.1/leads
Note that this has been successful only when issued directly against the odata v8.1 endpoint via postman.
When attempting to do the same, having a service running locally (Postman-->LocalHost Service-->CRM), this fails, and simply ignores??? the MSCRMCallerID header.
Upon examining headers that were passed to the LocalHost Microservice from Postman, the request, as examined by the debugger in VS 2017:
{Method: POST, RequestUri: 'https://.../api/data/v8.1/leads', Version: 1.1, Content: System.Net.Http.StringContent, Headers:
{
OData-Version: 4.0
OData-MaxVersion: 4.0
MSCRMCallerID: D994D6FF-5531-E711-9422-00155DC0D345
Cache-Control: no-cache
Accept: application/json
Content-Type: application/json; charset=utf-8
}}
The record is created succesfully, however on the CreatedBy field is the service username NOT the MSCRMCallerID username (d994d6ff-5531-e711-9422-00155dc0d345), and the CreatedOnBehalf field is empty.
What are we doing wrong?
How do we get this impersonation working from our service?
EDIT + More Info
Please note that I do believe that I've included all the relevant info, but if I have not, please let me know what other input I should provide on this issue.
What have I tried?
changed the order of headers
played with the case of the headers
ensured that the guid is correct of the user for impersonation
ensured that the user has both delegate and sys admin role (although this is irrelevant because this works when executing requesting directly against crm odata endpoint, rather than the endpoint that the our service exposes
have tried to execute the request against both https AND http
fiddler trace as shown below
Please note that this fiddler trace is a trace showing Postman --> Microservice request. It does not show the communication from the localhost microservice to CRM. (I'm not sure why, perhaps because it is encrypted)
POST https://localhost:19081/.....Leads/API/leads HTTP/1.1
Host: localhost:19081
Connection: keep-alive
Content-Length: 84
Cache-Control: no-cache
Origin: chrome-extension://aicmkgpgakddgnaphhhpliifpcfhicfo
MSCRMCallerID: D994D6FF-5531-E711-9422-00155DC0D345
X-Postman-Interceptor-Id: d79b1d2e-2155-f2ec-4ad7-e9b63e7fb90d
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8
Cookie: ai_user=Ka2Xn|2017-05-25T17:30:57.941Z
{
"subject": "created by mscrmcaller user2: d994d6ff-5531-e711-9422-00155dc0d345"
}
#Ram has suggested that we use the organization service to authenticate, is this an option, considering we are executing against Web API? Will the requested token still be valid. (Please note that this may be a silly question, and the reason is because I am not understanding how authentication works).
The following is a code snippet from how we are authenticating currently on every call:
//check headers to see if we got a redirect to the new location
var shouldAuthenticate = redirectUri.AbsoluteUri.Contains("adfs/ls");
if (!shouldAuthenticate)
{
return;
}
var adfsServerName = redirectUri.Authority;
var queryParams = HttpUtility.ParseQueryString(redirectUri.Query);
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, sslPolicyErrors) => true;
WSTrustChannelFactory factory = null;
try
{
// use a UserName Trust Binding for username authentication
factory = new WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
$"https://{adfsServerName}/adfs/services/trust/13/usernamemixed")
{
Credentials =
{
UserName =
{
UserName = $"{credential.Domain}\\{credential.UserName}",
Password = credential.Password
}
},
TrustVersion = TrustVersion.WSTrust13
};
var rst = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointReference(_client.BaseAddress.AbsoluteUri),
TokenType = "urn:oasis:names:tc:SAML:1.0:assertion",
KeyType = KeyTypes.Bearer
};
var channel = factory.CreateChannel();
channel.Issue(rst, out RequestSecurityTokenResponse rstr);
var fedSerializer = new WSFederationSerializer();
var rstrContent = fedSerializer.GetResponseAsString(rstr, new WSTrustSerializationContext());
// construct a authentication form
var crmauthenticaionPostDictionary = new Dictionary<string, string>
{
{"wa", queryParams["wa"]},
{"wresult", rstrContent},
{"wctx", queryParams["wctx"]}
};
// post the authentication form to the website.
var crmAuthorizationPostResponse = _client.PostAsync(_client.BaseAddress.AbsoluteUri, new FormUrlEncodedContent(crmauthenticaionPostDictionary)).Result;
var crmAuthorizationPostResponseString = crmAuthorizationPostResponse.Content.ReadAsStringAsync().Result;
//we should be authenticated here
if (
!(
// we are correctly authorized if we got redirected to the correct address that we
// were trying to reach in the first place.
crmAuthorizationPostResponse.StatusCode == HttpStatusCode.Redirect
&& crmAuthorizationPostResponse.Headers.Location == authenticationTestUri
)
)
{
throw new Exception("ADFS Authentication to CRM failed.");
}
When you are doing Postman to CRM request, its direct call & CRM handles it in expected way.
But in Postman -> Microservice -> CRM, the header get lost between Microservice to CRM.
In your Microservice, you have to handle the Header forward manually to CRM SDK call.
HttpWebRequest myHttpWebRequest1= (HttpWebRequest)WebRequest.Create(uri);
myHttpWebRequest1.Headers.Add("MSCRMCallerID", "D994D6FF-5531-E711-9422-00155DC0D345");
Or HTTP Header Forwarding (Sorry I could not find one for Azure / C#)
Update:
Am assuming you are following this MSDN samples to do your CRM web api call in c# microservice. I have included our header in need - MSCRMCallerID. See if it helps you.
public async Task BasicCreateAndUpdatesAsync()
{
Console.WriteLine("--Section 1 started--");
string queryOptions; //select, expand and filter clauses
//First create a new contact instance, then add additional property values and update
// several properties.
//Local representation of CRM Contact instance
contact1.Add("firstname", "Peter");
contact1.Add("lastname", "Cambel");
HttpRequestMessage createRequest1 =
new HttpRequestMessage(HttpMethod.Post, getVersionedWebAPIPath() + "contacts");
createRequest1.Content = new StringContent(contact1.ToString(),
Encoding.UTF8, "application/json");
createRequest1.Headers.Add("MSCRMCallerID", "D994D6FF-5531-E711-9422-00155DC0D345");
HttpResponseMessage createResponse1 =
await httpClient.SendAsync(createRequest1);
if (createResponse1.StatusCode == HttpStatusCode.NoContent) //204
{
Console.WriteLine("Contact '{0} {1}' created.",
contact1.GetValue("firstname"), contact1.GetValue("lastname"));
contact1Uri = createResponse1.Headers.
GetValues("OData-EntityId").FirstOrDefault();
entityUris.Add(contact1Uri);
Console.WriteLine("Contact URI: {0}", contact1Uri);
}
else
{
Console.WriteLine("Failed to create contact for reason: {0}",
createResponse1.ReasonPhrase);
throw new CrmHttpResponseException(createResponse1.Content);
}
}
There are fews things that you have to take care while impersonating
1. To impersonate a user, set the CallerId property on an instance of
OrganizationServiceProxy before calling the service’s Web methods.
2. The user (impersonator) must have the ActOnBehalfOf privilege or be a member of the PrivUserGroup group in Active Directory
Code Example
SystemUser user = null;
user = new SystemUser(systemUser);
OrganizationServiceProxy service = CrmService.Proxy;
service.CallerID = user.Id;
Since your code is not available please ensure all the above fields are set properly
For detailed understanding use the link
https://crmbusiness.wordpress.com/2015/07/21/crm-2015-understanding-impersonation-in-plugins-and-knowing-when-to-use-it/
I need to perform few Azure SQL operations. I have an Azure AD native application. I'm using first approach from following article to aquire the token.
https://msdn.microsoft.com/en-us/library/azure/ee460782.aspx
Now following this article, I'm using the above token to perform the db operation.
static void HttpPost(string sourceDb, string targetDb, string pointInTime)
{
var client = new HttpClient();
string uri = "https://management.core.windows.net:8443/" + AzureSubscriptionId + "/services/sqlservers/servers/" + AzureSqlServerName + "/restoredatabaseoperations";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Headers.Add("Authorization", "Bearer " + accessToken);
request.Headers.Add("x-ms-version", "2012-03-01");
string payload = File.ReadAllText("Resources\\Backup.xml");
payload = payload.Replace("$SourceDb", sourceDb);
payload = payload.Replace("$TargetDb", targetDb);
payload = payload.Replace("$PointInTime", pointInTime);
request.Content = new StringContent(payload, Encoding.UTF8, "application/xml");
HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult();
if (response.Content != null)
{
string ss = response.Content.ReadAsStringAsync().Result;
}
}
But the error I receive is:
"<Error xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Code>AuthenticationFailed</Code><Message>A security token exception occured for the received JWT token.</Message></Error>"
According to you mentioned Create Database Restore Request (Classic) REST API, this command is used for classic deployment model. We should use the new REST API, and it is also mentioned in your mentioned document.
You should use the newer Resource Manager based REST API commands located here.
We could use the ARM REST Create or Update DataBase API. About how to get the token, we need to to registry AD App and assign role to application, more info please refer to official document. I send the http request from fiddler and it works correctly for me. Header and body info please refer to the screenshot.
Body info:
{
"properties": {
"edition": "Standard",
"requestedServiceObjectiveName": "S1",
"sourceDatabaseId": "/subscriptions/{your subscriptionId}/resourceGroups/{ResourceGroup}/providers/Microsoft.Sql/servers/{servername}/databases/sourcedatabasename",
"createMode": "PointInTimeRestore",
"restorePointInTime": "2017-02-09T10:28:20.21+08:00" //source database restorePointTime
},
"location": "East Asia",
"tags": {}
}
We also can use Microsoft Azure SQL Management Library for .NET to that.
SqlMgmtClient.Databases.CreateOrUpdate(resourceGroupName, serverName, databaseName, DatabaseCreateOrUpdateParameters);
We could refer to the tutorial to get started. I do a demo for it. More details please refer to the following steps
1.Create a console app and install the required libraries (detail refer to tutorial)
2.After registry an application, we could get tenantId, applicationId,
SecretKey, then with subscriptionId to get the authentication token.
3.Create SqlManagementClient object with token
var _sqlMgmtClient = new SqlManagementClient(new TokenCloudCredentials(_subscriptionId, _token.AccessToken));
4.Create DatabaseCreateOrUpdateParameters according to our requirement.
Take restore database from a source database for example:
CreateMode = DatabaseCreateMode.PointInTimeRestore, //craete mode from pointtimerestore
Edition = databaseEdition,
SourceDatabaseId = "/subscriptions/subscriptionId/resourceGroups/groupname/providers/Microsoft.Sql/servers/AzureSQlname/databases/databaseName", //source database Id
RestorePointInTime = DateTime.Parse("2017-02-09T02:28:20.21Z"), //resore point Time
RequestedServiceObjectiveName = "S1"
run the demo and check from the portal.
democode:
static void Main(string[] args)
{
_token = GetToken(_tenantId, _applicationId, _applicationSecret);
Console.WriteLine("Token acquired. Expires on:" + _token.ExpiresOn);
// Instantiate management clients:
_resourceMgmtClient = new ResourceManagementClient(new Microsoft.Rest.TokenCredentials(_token.AccessToken));
_sqlMgmtClient = new SqlManagementClient(new TokenCloudCredentials(_subscriptionId, _token.AccessToken));
DatabaseCreateOrUpdateResponse dbr = CreateOrUpdateDatabase(_sqlMgmtClient, _resourceGroupName, _serverName, _databaseName, _databaseEdition, _databasePerfLevel);
Console.WriteLine("Database: " + dbr.Database.Id);
}
private static AuthenticationResult GetToken(string tenantId, string applicationId, string applicationSecret)
{
AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/" + tenantId);
_token = authContext.AcquireToken("https://management.core.windows.net/", new ClientCredential(applicationId, applicationSecret));
return _token;
}
static DatabaseCreateOrUpdateResponse CreateOrUpdateDatabase(SqlManagementClient sqlMgmtClient, string resourceGroupName, string serverName, string databaseName, string databaseEdition, string databasePerfLevel)
{
// Retrieve the server that will host this database
Server currentServer = sqlMgmtClient.Servers.Get(resourceGroupName, serverName).Server;
// Create a database: configure create or update parameters and properties explicitly
DatabaseCreateOrUpdateParameters newDatabaseParameters = new DatabaseCreateOrUpdateParameters()
{
Location = currentServer.Location,
Properties = new DatabaseCreateOrUpdateProperties
{
CreateMode = DatabaseCreateMode.PointInTimeRestore,
Edition = databaseEdition,
SourceDatabaseId = "/subscriptions/subscriptionId/resourceGroups/tomnewgroup/providers/Microsoft.Sql/servers/tomsunsqltest/databases/sourceDatabaseName",
RestorePointInTime = DateTime.Parse("2017-02-09T02:28:20.21Z"),//Restore Point time
RequestedServiceObjectiveName = databasePerfLevel
}
};
DatabaseCreateOrUpdateResponse dbResponse = sqlMgmtClient.Databases.CreateOrUpdate(resourceGroupName, serverName, databaseName, newDatabaseParameters);
return dbResponse;
}
packages.config file:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Hyak.Common" version="1.0.2" targetFramework="net462" />
<package id="Microsoft.Azure.Common" version="2.1.0" targetFramework="net462" />
<package id="Microsoft.Azure.Common.Authentication" version="1.7.0-preview" targetFramework="net462" />
<package id="Microsoft.Azure.Common.Dependencies" version="1.0.0" targetFramework="net462" />
<package id="Microsoft.Azure.Management.ResourceManager" version="1.4.0-preview" targetFramework="net462" />
<package id="Microsoft.Azure.Management.Sql" version="0.51.0-prerelease" targetFramework="net462" />
<package id="Microsoft.Bcl" version="1.1.9" targetFramework="net462" />
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net462" />
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net462" />
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="2.18.206251556" targetFramework="net462" />
<package id="Microsoft.Net.Http" version="2.2.22" targetFramework="net462" />
<package id="Microsoft.Rest.ClientRuntime" version="2.1.0" targetFramework="net462" />
<package id="Microsoft.Rest.ClientRuntime.Azure" version="3.1.0" targetFramework="net462" />
<package id="Microsoft.Rest.ClientRuntime.Azure.Authentication" version="2.0.1-preview" targetFramework="net462" />
<package id="Newtonsoft.Json" version="6.0.8" targetFramework="net462" />
</packages>
Please open a support case allowing us to better understand the issue
I was trying to follow the steps from this this tutorial to authenticate my app by oAuth and use the retrieved token for EWS managed API.
Here is the final code:
static void Run()
{
string authority = ConfigurationSettings.AppSettings["authority"];
string clientID = ConfigurationSettings.AppSettings["clientID"];
Uri clientAppUri = new Uri(ConfigurationSettings.AppSettings["clientAppUri"]);
string serverName = ConfigurationSettings.AppSettings["serverName"];
AuthenticationResult authenticationResult = null;
AuthenticationContext authenticationContext = new AuthenticationContext(authority, false);
string errorMessage = null;
try
{
Console.WriteLine("Trying to acquire token");
authenticationResult = authenticationContext.AcquireToken(serverName, clientID, clientAppUri, PromptBehavior.Auto);
}
catch (AdalException ex)
{
errorMessage = ex.Message;
if (ex.InnerException != null)
{
errorMessage += "\nInnerException : " + ex.InnerException.Message;
}
}
catch (ArgumentException ex)
{
errorMessage = ex.Message;
}
if (!string.IsNullOrEmpty(errorMessage))
{
Console.WriteLine("Failed: {0}" + errorMessage);
return;
}
Console.WriteLine("\nMaking the protocol call\n");
ExchangeService exchangeService = new ExchangeService(ExchangeVersion.Exchange2013);
exchangeService.Url = new Uri(serverName + "ews/exchange.asmx");
exchangeService.TraceEnabled = true;
exchangeService.TraceFlags = TraceFlags.All;
exchangeService.Credentials = new OAuthCredentials(authenticationResult.AccessToken);
//exchangeService.TraceListener = new TraceListener();
var res = exchangeService.FindFolders(WellKnownFolderName.Root, new FolderView(10));
}
and here is the configuration file:
<add key="authority" value="https://login.windows.net/???.onmicrosoft.com" />
<add key="clientId" value="???" />
<add key="clientAppUri" value="https://localhost/8a4abb13c70dab64a18ae81089bc2cff"/>
<add key="serverName" value="https://outlook.office365.com/" />
Here is the error message:
The request failed. The remote server returned an error: (401) Unauthorized.
and here is trace of ExchangeService:
<Trace Tag="EwsRequestHttpHeaders" Tid="12" Time="2015-08-13 13:56:24Z">
POST /ews/exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0847.030
Accept-Encoding: gzip,deflate
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1iYTlnb0VLWSIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1iYTlnb0VLWSJ9.eyJhdWQiOiJodHRwczovL291dGxvb2sub2ZmaWNlMzY1LmNvbS8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9mMTFjYjAzOS05NzFlLTQxOWItYjg5NS01ZjIxZTkxOWFlZmUvIiwiaWF0IjoxNDM5NDczOTI0LCJuYmYiOjE0Mzk0NzM5MjQsImV4cCI6MTQzOTQ3NzgyNCwidmVyIjoiMS4wIiwidGlkIjoiZjExY2IwMzktOTcxZS00MTliLWI4OTUtNWYyMWU5MTlhZWZlIiwiZW1haWwiOiJoYW1pZC5lbG1pQGNvbnNldHRvLmNvbSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2JiZDIwM2I4LTkwMjEtNDZiZi05NzE2LTUyZWE5MzY1MjQ3Zi8iLCJhbHRzZWNpZCI6IjU6OjEwMDNCRkZEOEIxMTQxMTUiLCJzdWIiOiJPc0t2cnY3YU1hX3VKZktYNGltYXduSVp6SVIybDdkTXBkcUV6M0Y2U2U0IiwiZ2l2ZW5fbmFtZSI6IkhhbWlkIiwibmFtZSI6IkhhbWlkIEVsbWkiLCJhbXIiOlsicHdkIl0sInVuaXF1ZV9uYW1lIjoiaGFtaWQuZWxtaUBjb25zZXR0by5jb20iLCJhcHBpZCI6IjFkMGFiN2ZmLTU5NzItNDJlZS1iZGMwLWYzMzQwNTJhZmZjOCIsImFwcGlkYWNyIjoiMCIsInNjcCI6IkNhbGVuZGFycy5SZWFkIENhbGVuZGFycy5SZWFkV3JpdGUgQ29udGFjdHMuUmVhZCBDb250YWN0cy5SZWFkV3JpdGUgRGlyZWN0b3J5LkFjY2Vzc0FzVXNlci5BbGwgRGlyZWN0b3J5LlJlYWQgRGlyZWN0b3J5LldyaXRlIEZpbGVzLlJlYWQgRmlsZXMuUmVhZC5TZWxlY3RlZCBGaWxlcy5SZWFkV3JpdGUgRmlsZXMuUmVhZFdyaXRlLlNlbGVjdGVkIGZ1bGxfYWNjZXNzX2FzX3VzZXIgR3JvdXAuUmVhZC5BbGwgR3JvdXAuUmVhZFdyaXRlLkFsbCBNYWlsLlJlYWQgTWFpbC5SZWFkV3JpdGUgTWFpbC5TZW5kIG9mZmxpbmVfYWNjZXNzIG9wZW5pZCBTaXRlcy5SZWFkLkFsbCBTaXRlcy5SZWFkV3JpdGUuQWxsIFVzZXIuUmVhZCBVc2VyLlJlYWQuQWxsIFVzZXIuUmVhZEJhc2ljLkFsbCBVc2VyLlJlYWRXcml0ZSBVc2VyLlJlYWRXcml0ZS5BbGwiLCJhY3IiOiIxIn0.tZAyNFquVvyg46lsN79bmpdHhVEPwCIbBXfgsQ3kCzXgmf0LmX3s0A6SV7eSfEKef_-U78HBViAIaUexWeKAV0SKzJZUiQJ0dpDossYt6CfBAlFn4J6_5oZ_jygeNH3xeiCgU4tQrlz5t8iMOeSmBjwIsa2K-Sizd_zC8m3wptg6HI2ubdFJd0VXYqb7WFW_Sb-7wmOZqp8Lybpf3W6qRO14FRpm1f0RGec7kx4jd0EobPUaYWnQaet2I8P-5tuq6fmkJx78mQLGCrkZMcSJVakWVgepgO3LrEvKRKLuzJ9p5fiRiEVGNAzHUubrIjCVcXuquYaPdF16dK3gUT3Uiw
</Trace>
<Trace Tag="EwsRequest" Tid="12" Time="2015-08-13 13:56:24Z" Version="15.00.0847.030">
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<t:RequestServerVersion Version="Exchange2013" />
</soap:Header>
<soap:Body>
<m:FindFolder Traversal="Shallow">
<m:FolderShape>
<t:BaseShape>AllProperties</t:BaseShape>
</m:FolderShape>
<m:IndexedPageFolderView MaxEntriesReturned="10" Offset="0" BasePoint="Beginning" />
<m:ParentFolderIds>
<t:DistinguishedFolderId Id="root" />
</m:ParentFolderIds>
</m:FindFolder>
</soap:Body>
</soap:Envelope>
</Trace>
A first chance exception of type 'System.Net.WebException' occurred in System.dll
<Trace Tag="EwsResponseHttpHeaders" Tid="12" Time="2015-08-13 13:56:26Z">
HTTP/1.1 401 Unauthorized
request-id: ea025c98-9e74-4799-8ac2-77251f641912
X-CalculatedBETarget: HE1PR09MB0330.eurprd09.prod.outlook.com
X-BackEndHttpStatus: 401
Content-Length: 0
Set-Cookie: ClientId=4HGSRI510SWOAIFT0U31G; expires=Fri, 12-Aug-2016 13:57:17 GMT; path=/; secure; HttpOnly,exchangecookie=86f12da3fb89403bb91c5fe8b525f43d; expires=Sat, 13-Aug-2016 13:57:18 GMT; path=/; HttpOnly,ClientId=4HGSRI510SWOAIFT0U31G; expires=Fri, 12-Aug-2016 13:57:17 GMT; path=/; secure; HttpOnly
Server: Microsoft-IIS/8.0
x-ms-diagnostics: 2000001;reason="No applicable user context claims found.";error_category="invalid_token"
X-DiagInfo: HE1PR09MB0330
X-BEServer: HE1PR09MB0330
X-Powered-By: ASP.NET
X-FEServer: DB5PR09CA0061
WWW-Authenticate: Bearer client_id="00000002-0000-0ff1-ce00-000000000000", trusted_issuers="00000001-0000-0000-c000-000000000000#*", token_types="app_asserted_user_v1", authorization_uri="https://login.windows.net/common/oautA first chance exception of type 'Microsoft.Exchange.WebServices.Data.ServiceRequestException' occurred in Microsoft.Exchange.WebServices.dll
h2/authorize", error="invalid_token",Basic Realm="",Basic Realm=""
Date: Thu, 13 Aug 2015 13:57:17 GMT
</Trace>
Any idea would be greatly appreciated.
Your token has too many scopes in it (i.e. the scp claim). That value should only be full_access_as_user. In the Azure Management Portal, the only Office 365 Exchange Online permission you should have is "Access mailboxes as the signed-in user via Exchange Web Services".
after redirect to facebook for getting code token and when facebook redirect back to me, my app stop responding, action, which should be called never fires up. where is the problem?
my routes
routes.MapRoute(
"Enter facebook return",
"enter/facebook/return",
new { controller = "Users", action = "FacebookReturn" } //callback action, which not called -> problem
);
routes.MapRoute(
"Enter facebook",
"enter/facebook",
new { controller = "Users", action = "Facebook" }
);
and my controller
public ActionResult Facebook()
{
OAuth2 facebook = new OAuth2(
ConfigurationManager.AppSettings["oauth_facebook_id"].ToString(),
ConfigurationManager.AppSettings["oauth_facebook_secret"].ToString(),
ConfigurationManager.AppSettings["oauth_facebook_authorize"].ToString(),
ConfigurationManager.AppSettings["oauth_facebook_access"].ToString(),
ConfigurationManager.AppSettings["oauth_facebook_return"].ToString()
);
Session["enter_facebook"] = facebook; // save data to session
Dictionary<string, string> p = new Dictionary<string, string> { { "scope", "email" } };
string redirectURL = facebook.GetAuthCode(p); //redirect url
//redirect url looks like https://www.facebook.com/dialog/oauth?client_id=111111111&response_type=code&redirect_uri=http%3A%2F%localhost.loc%2Fenter%2Ffacebook%2Freturn&scope=email
return Redirect(redirectURL);
}
public ActionResult FacebookReturn(string code)
{
if (!string.IsNullOrEmpty(code))
{
//after returning to us
OAuth2 facebook = (OAuth2)Session["enter_facebook"];
facebook.Code = code; //getting code
Dictionary<string, string> p = new Dictionary<string, string> { { "client_secret", ConfigurationManager.AppSettings["oauth_facebook_secret"].ToString() } }; //additional params
OAuth2Token token = facebook.GetAccessToken(p, OAuth2.AccessTokenType.Dictionary); //getting marker access
string access_token = token.dictionary_token["access_token"];
if (!string.IsNullOrEmpty(access_token))
{
string user_data = OAuth2UserData.GetFacebookUserData(access_token); //getting data about user
string a = "";
}
}
return View("Enter");
}
my livehhttpheader
GET https://www.facebook.com/dialog/oauth?client_id=111111111&response_type=code&redirect_uri=http%3A%2F%localhost.loc%2Fenter%2Ffacebook%2Freturn&scope=email
Host: www.facebook.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:23.0) Gecko/20100101 Firefox/23.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://localhost.loc/enter
Connection: keep-alive
so at the end of this i see when i click on "login with facebook":
1.https://www.facebook.com/dialog/oauth?client_id=111111111&response_type=code&redirect_uri=http%3A%2F%localhost.loc%2Fenter%2Ffacebook%2Freturn&scope=email
2.http://localhost.loc/enter/facebook/return?code=2222222222
3.my app stopped here, FacebookReturn(string code) never called, can't get address in p.2 (sign in firefox as loading and never ends), BUT if just call p.2 without redirect everything is fine
so question is: why after redirecting from facebook to my site action never calles and it seems like in application pool some problems