Share via


Troubleshooting and developing oauth enabled applications with Dynamics 365

OAuth applications can be developed using Dynamics 365 Online and OnPremise versions. Let's see these in more details:

HOW DOES OAUTH WORK IN GENERAL oauth-flow

 

  • The Authorization Server is the v2.0 endpoint. It is responsible for ensuring the user's identity, granting and revoking access to resources, and issuing tokens. It is also known as the identity provider - it securely handles anything to do with the user's information, their access, and the trust relationships between parties in an flow.
  • The Resource Owner is typically the end-user. It is the party that owns the data, and has the power to allow third parties to access that data, or resource.
  • The OAuth Client is your app, identified by its Application Id. It is usually the party that the end-user interacts with, and it requests tokens from the authorization server. The client must be granted permission to access the resource by the resource owner.
  • The Resource Server is where the resource or data resides. It trusts the Authorization Server to securely authenticate and authorize the OAuth Client, and uses Bearer access_tokens to ensure that access to a resource can be granted.

You can read the complete detail on Microsoft Azure Documentation:

UNDERSTANDING THE FLOW OF YOUR APPLICATION

Now in our scenario we will register our c# application in azure management portal. Read more to create apps in C#. Let's go and see how will this work in real time. I will demonstrate this through an image

realtime-flow

Let's dissect to understand the parameter values:

  • Client ID : 32-Digit Guid registered in Azure
  • Resource Url: Fully Qualified Dynamics 365 Url (ex: orgname.crm.dyamics.com)
  • Oauth Endpoint: Fully Qualified Azure Oauth endpoint url (should be retrieved dynamically). See this post: Sample: https://login.windows.net/<tenantId>/oauth2/authorize?

Passing these to ADAL Library will prompt user with Office 365 Logon page.

You can visit Developer Center to see how to obtain Dynamics 365 Endpoint Urls: https://www.microsoft.com/en-us/dynamics/crm-customer-center/view-or-download-developer-resources.aspx.

Each function written in ADAL and when used in c# application is an Async function. Which means you will be using Task (System.Threading.Tasks.Task) and if the application fails to connect or authenticate you will receive "Task has been canceled" exception. Then you will need to expand the exception dialog in Visual Studio to see the inner exception.

See example:

 Program app = new Program();
 Task.WaitAll(Task.Run(async () => await app.CreateMyReordsAsync())); 

Function definition

 async Task CreateMyReordsAsync()
        {
            WebApiOperationHelper apihelper = new WebApiOperationHelper();
            apihelper.BaseOrganizationApiUrl = "https://org.api.crm.dynamics.com";
            apihelper.ObtainOAuthToken();  //Let's say this step failed. 

            if (!(string.IsNullOrEmpty(apihelper.AccessToken)))
            { //Success go }

UNDERSTANDING FIDDLER REQUESTS FOR YOUR APPLICATION

Let's see the complete application flow in fiddler

#   Result  Protocol  Host  URL  Body  Caching  Content-Type 
2  200  HTTP  Tunnel to  orgname.crm.dynamics.com:443     
3  401  HTTPS  orgname.crm.dynamics.com  /api/data/v8.1  49    text/html 
4  200  HTTP  Tunnel to  login.windows.net:443     
5  302  HTTPS  login.windows.net  /<tenantid>/oauth2/authorize?resource=https%3A%2F%2Forgname.crm.dynamics.com%2F&client_id=<clientid>&response_type=code&redirect_uri=http%3A%2F%2Fgo%2F&client-request-id=4e9ee0d9-516a-455a-9f5d-373eea250208&prompt=login&x-client-SKU=.NET&x-client-Ver=2.22.0.0&x-client-CPU=x64&x-client-OS=Microsoft+Windows+NT+6.2.9200.0  391  private  text/html; charset=utf-8 
6  200  HTTP  Tunnel to  login.microsoftonline.com:443     
7  200  HTTPS  login.microsoftonline.com  /<tenantId>/oauth2/authorize?resource=https%3A%2F%2Forgname.crm.dynamics.com%2F&client_id=<clientid>&response_type=code&redirect_uri=http%3A%2F%2Fgo%2F&client-request-id=4e9ee0d9-516a-455a-9f5d-373eea250208&prompt=login&x-client-SKU=.NET&x-client-Ver=2.22.0.0&x-client-CPU=x64&x-client-OS=Microsoft+Windows+NT+6.2.9200.0  12,572  no-cache, no-store; Expires: -1  text/html; charset=utf-8 
8  200  HTTPS  login.microsoftonline.com  /common/instrumentation/reportpageload  264  private  application/json; charset=utf-8 
9  200  HTTPS  login.microsoftonline.com  /common/userrealm/?user=admin%40orgname.onmicrosoft.com&api-version=2.1&stsRequest=rQIIAePiMNIzMtIz0DPQYjbUM7RSMTEzNk82S03UNU9JNNA1SU5J0k1MSjXUNTFISzUyM041NDYyLBLiErj5YJ5fVmCU6_xYc7tXqkwcqxi5MkpKCqz09dPz9XcwMl5gZHzByNjAxHiLid_fsbQkwwhE5BdlVqU-whB5xcSak5-emTeJmT8_ESSjByKT81NSVzErgowtBpqbWFBaVJaeoZdclKuXUpmXmJuZXKyXnJ-rv4lZxTDJzDI12dxUNy3ZAujWxDRD3cQ0U3PdVFMz8xSz1BRzYwvjXcwqFmYmyckGBom6SWmmqbomRgapQM-ZmumaG1qmGBsmpqYkp6TcYGa8wML4ioWHg0lATIJBgUGDxYDvBwvjIlagr1fyt-VacOp4zP37aKfB84cOp1j13YMrDBIrMgKMK42NK9LDsgu8LMIi_FyCykNyCi0j_LMzfavCPPw8SkotHW3NrQx3cSKFEwA1&checkForMicrosoftAccount=true  237  private  application/json; charset=utf-8 
10  200  HTTP  Tunnel to  2-edge-chat.facebook.com:443     
11  200  HTTPS  2-edge-chat.facebook.com  /pull?channel=p_730503133&seq=28&partition=-2&clientid=4b5b6dbc&cb=gayo&idle=320&qp=y&cap=8&pws=fresh&isq=9879&msgs_recv=28&uid=730503133&viewer_uid=730503133&sticky_token=4&sticky_pool=ash3c07_chat-proxy  28  private, no-store, no-cache, must-revalidate  application/json 
12  302  HTTPS  login.microsoftonline.com  /<tenantid>/login  741  no-cache, no-store; Expires: -1  text/html; charset=utf-8 
13  200  HTTP  Tunnel to  login.windows.net:443     
14  200  HTTPS  login.windows.net  /<tenantid>/oauth2/token  3,148  no-cache, no-store; Expires: -1  application/json; charset=utf-8 
15  200  HTTP  Tunnel to  orgname.crm.dynamics.com:443     
16  200  HTTPS  orgname.crm.dynamics.com  /api/data/v8.1/contacts?$select=firstname&$filter=contains(firstname,'Peter')  124  no-cache; Expires: -1  application/json; odata.metadata=minimal 

 Let's double click Frame 3#

GET https://login.windows.net/\<ClientID/oauth2/authorize?resource=https%3A%2F%2Forgname.crm.dynamics.com%2F&client_id=<ClientId>&response_type=code&redirect_uri=http%3A%2F%2Fgo%2F&client-request-id=4e9ee0d9-516a-455a-9f5d-373eea250208&prompt=login&x-client-SKU=.NET&x-client-Ver=2.22.0.0&x-client-CPU=x64&x-client-OS=Microsoft+Windows+NT+6.2.9200.0 HTTP/1.1
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)
Host: login.windows.net
Connection: Keep-Alive

This is the request sent to universal online URL. When we go and see the result in Fame 14# , see the response:

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
client-request-id: 4e9ee0d9-516a-455a-9f5d-373eea250208
x-ms-request-id: 4178e998-36e1-4dd7-ab56-8ebd0936904a
P3P: CP="DSP CUR OTPi IND OTRi ONL FIN"
Set-Cookie: esctx=AQABAAAAAADRNYRQ3dhRSrm-4K-adpCJdtApztEfq-GRTdvfueyNhBbkavXHFfnJCcP7lrKEtfnxxYThlX9R_wwMC_36VsfpmQe2a-K8OoAeJfc0EfP1o2SRDXwCPkmzHMDp5pU7XnFBDyupj7pXj2YSavw-coII5LgmagCSKlZCPG_ZBiygejDCDTdzsbtBG0tv1QXNnikgAA; domain=.login.windows.net; path=/; secure; HttpOnly
Set-Cookie: x-ms-gateway-slice=006; path=/; secure; HttpOnly
Set-Cookie: stsservicecookie=ests; path=/; secure; HttpOnly
X-Powered-By: ASP.NET
Date: Sun, 11 Dec 2016 16:12:57 GMT
Content-Length: 3148

{"token_type":"Bearer","scope":"user_impersonation","expires_in":"3599","ext_expires_in":"10800","expires_on":"1481476378","not_before":"1481472478","resource":"https://orgname.crm.dynamics.com/","access_token":" <json response.> "}

If there was an error, you would have seen an error message in Json format thrown by Azure. Let's decrypt your Json token to understand what does that mean; Copy the value and go to https://jwt.calebb.net/. (Third-Party Website).  You will see the following content when you decrypt the token:
{ typ: "JWT", alg: "RS256", x5t: "RrQqu9rydBVRWmcocuXUb20HGRM", kid: "RrQqu9rydBVRWmcocuXUb20HGRM" }. { aud: "https://Orgname.crm.dynamics.com/", iss: "https://sts.windows.net/<ClientId>/", iat: 1481472478, nbf: 1481472478, exp: 1481476378, acr: "1", amr: [ "pwd" ], appid: "<<ClientId >>", appidacr: "0", e_exp: 10800, family_name: "Ghai", given_name: "Apurv", ipaddr: "00.00.00.00", name: "Apurv Ghai", oid: "99914ea0-1e03-4850-a963-8f240a87d152", platf: "14", puid: "1003BFFD97A8CE14", scp: "user_impersonation", sub: "tzNxkMSUdSHCNt29AQg3adxxQTkhE7-wXc6woLesVnY", tid: "1b69ec75-fc81-4af1-af57-e567d6ed7383", unique_name: "admin@domain.onmicrosoft.com", upn: "admin@domain.onmicrosoft.com", ver: "1.0", wids: [ "62e90394-69f5-4237-9190-012177145e10" ] }. [signature]
This json format confirms the credentials of the logged on user. This way you can really understand what's going on with your application. Further now, this token will be used to make webapi requests in Dynamics 365,

# Result Protocol Host URL Body Caching Content-Type Process Comments Custom
16 200 HTTPS orgname.crm.dynamics.com /api/data/v8.1/contacts?$select=firstname&$filter=contains(firstname,'Peter') 124 no-cache; Expires: -1 application/json; odata.metadata=minimal crm_sdk_samples:7448

In Frame 16# , you see that we are doing API Call to Dynamics 365. Here's the header format:

GET https://orgname.crm.dynamics.com/api/data/v8.1/contacts?$select=firstname&$filter=contains(firstname,'Peter') HTTP/1.1
Authorization: Bearer <encrypted part removed>
Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
Cache-Control: no-cache
Host: orgname.crm.dynamics.com

Let's map this request with the piece of code:

 public async Task SearchExistingRecord(string entityName, string filter)
        {
            httpClient = CreateDynHttpClient(AccessToken, entityName);<br>            string completedFilterCondition = BaseOrganizationApiUrl + "/api/data/v8.1/" + entityName + filter;  
            var response = await httpClient.GetAsync(completedFilterCondition);
            response.EnsureSuccessStatusCode();
            if (response.StatusCode == HttpStatusCode.OK)
            {
                var content = await response.Content.ReadAsStringAsync();
                var objParsedContent = JsonConvert.DeserializeObject(content);

                // Do something with response. Example get content:
                Console.WriteLine(objParsedContent);
                Console.WriteLine("Records Found");
                Console.ReadKey();
                //Dispose the Object :: Best Practice
                httpClient.Dispose();
            }
        }

if you see the highlighted portion after creating the HttpClient we are passing the access token to be sent to every request going to Dynamics 365. Here's the completed response to this request:
HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; odata.metadata=minimal Expires: -1 Server: Microsoft-IIS/8.5 REQ_ID: 38e4f5e9-7fd8-4ac6-822f-9631519f08d5 OData-Version: 4.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET Date: Sun, 11 Dec 2016 16:12:58 GMT Content-Length: 124 Set-Cookie: crmf5cookie=!x8uRQEWsVflwvJeyl31zE1vVQ1hrKpIoEAcC4gh9O0uumcDINf2R/MfwM5l2UsVA7AVQX3hQVwkwNNk=;secure; path=/ Strict-Transport-Security: max-age=31536000; includeSubDomains { "@odata.context":"https://orgname.crm.dynamics.com/api/data/v8.1/$metadata#contacts(firstname)","value":[ ] }
Basically, there was no data found with that criteria specified as first name = Peter.

Hope you've enjoyed reading this.

Happy WebAPI'ing

Cheers,

Apurv :)