2016-12-11 | apurvghai | Dynamics CRM SDK | C#
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
- 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
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 |
0 |
|
|
3 |
401 |
HTTPS |
orgname.crm.dynamics.com |
/api/data/v8.1 |
49 |
|
text/html |
4 |
200 |
HTTP |
Tunnel to |
login.windows.net:443 |
0 |
|
|
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 |
0 |
|
|
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 |
0 |
|
|
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 |
0 |
|
|
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 |
0 |
|
|
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 http://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);
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”:[
]
}
</code>
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 :)