The Problem:
I am struggeling to understand how to get tokens. I know why I should use them, but I just don't understand how to get them. All the samples that uses Tokens just fetch them from "https://webchat-mockbot.azurewebsites.net/directline/token" or something similar. How do I create this path in my bot?
Describe alternatives you have considered
I was able to create something which worked with my JS-Bot:
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function() {
console.log(`\n${ server.name } listening to ${ server.url }`);
console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator');
console.log('\nTo talk to your bot, open the emulator select "Open Bot"');
});
server.post('/token-generate', async (_, res) => {
console.log('requesting token ');
try {
const cres = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', {
headers: {
authorization: `Bearer ${ process.env.DIRECT_LINE_SECRET }`
},
method: 'POST'
});
const json = await cres.json();
if ('error' in json) {
res.send(500);
} else {
res.send(json);
}
} catch (err) {
res.send(500);
}
});
But I don't find how to do this with my C#-Bot ( I switched to C# because I understand it better than JS).
In my C#-Bot there is only this:
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
namespace ComplianceBot.Controllers
{
// This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot
// implementation at runtime. Multiple different IBot implementations running at different endpoints can be
// achieved by specifying a more specific type for the bot constructor argument.
[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly IBot _bot;
public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
{
_adapter = adapter;
_bot = bot;
}
[HttpGet, HttpPost]
public async Task PostAsync()
{
// Delegate the processing of the HTTP POST to the adapter.
// The adapter will invoke the bot.
await _adapter.ProcessAsync(Request, Response, _bot);
}
}
}
Can I add a new Route here? like [Route("directline/token")] ?
I know I could do this with an extra "token-server" (I don't know how to realise it, but I know that would work), but if possible I'd like to do this with my already existing c#-bot as I did it with my JS-Bot.
I have posted an answer which includes how to implement an API to get a direct line access token in C# bot and how to get this token, just refer to here. If you have any further questions, pls feel free to let me know .
Update :
My code is based on this demo . If you are using .net core, pls create a TokenController.cs under /Controllers folder:
Code of TokenController.cs :
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace Microsoft.BotBuilderSamples.Controllers
{
[Route("api/token")]
[ApiController]
public class TokenController : ControllerBase
{
[HttpGet]
public async Task<ObjectResult> getToken()
{
var secret = "<direct line secret here>";
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
$"https://directline.botframework.com/v3/directline/tokens/generate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);
var userId = $"dl_{Guid.NewGuid()}";
request.Content = new StringContent(
Newtonsoft.Json.JsonConvert.SerializeObject(
new { User = new { Id = userId } }),
Encoding.UTF8,
"application/json");
var response = await client.SendAsync(request);
string token = String.Empty;
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
}
var config = new ChatConfig()
{
token = token,
userId = userId
};
return Ok(config);
}
}
public class DirectLineToken
{
public string conversationId { get; set; }
public string token { get; set; }
public int expires_in { get; set; }
}
public class ChatConfig
{
public string token { get; set; }
public string userId { get; set; }
}
}
Run the project after you replace secret with your own direct line secret. You will be able to get token by url: http://localhost:3978/api/token on local :
Related
I have been having this problem for some time so to reproduce the error I created a new Blazor project to show you all the files.
This is my index.razor file:
#using BlazorApp1.Models.TestSuiteModel;
#inject BlazorApp1.Services.TestSuiteService TestSuiteServ;
#page "/"
<b>
#TestSuitesResult.count;
</b>
#code {
protected override async Task OnInitializedAsync()
{
this.GetTestSuites();
}
TestSuiteModel TestSuitesResult;
protected async Task GetTestSuites()
{
TestSuiteServ.url = "https://dev.azure.com/****/_apis/test/Plans/12/suites?api-version=5.0";
TestSuiteServ.PersonalAccessToken = "****";
TestSuitesResult = await TestSuiteServ.GetTestSuites();
await base.OnInitializedAsync();
}
}
The problem is #TestSuitesResult.count; is always null and throws this error:
Below is my Model
namespace BlazorApp1.Models.TestSuiteModel
{
public class TestSuiteModel
{
public List<Value> value { get; set; }
public int count { get; set; }
}
// .. mode classes
}
Below is my Service
using BlazorApp1.Models.TestSuiteModel;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Net.Http.Headers;
namespace BlazorApp1.Services
{
public class TestSuiteService
{
public string url { get; set; }
public string PersonalAccessToken { get; set; }
TestSuiteModel result;
public async Task<TestSuiteModel> GetTestSuites()
{
using (HttpClient client = new HttpClient())
{
// accept response as JSON
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json")
);
// add DevOps token to the HTTP Header request
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes(
string.Format("{0}:{1}", "", PersonalAccessToken)
)
)
);
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseData = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TestSuiteModel>(responseData);
}
}
}
}
That is the only thing I have in this project. I didn't modify or delete any file from the default Blazor project except registering the service in program.cs
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddSingleton<TestSuiteService>(); <!-- my service -->
var app = builder.Build();
Also Newtonsoft is added so it could not be a missing library.
GetTestSuites is never awaited
protected override async Task OnInitializedAsync()
{
this.GetTestSuites();
}
It should be :
protected override async Task OnInitializedAsync()
{
await this.GetTestSuites();
}
Right now OnInitializedAsync completes and the page is rendered before GetTestSuites has a chance to complete.
There are other problems with TestSuiteService too. HttpClient is meant to be reused, not defined in a using block. Especially in Blazor, the HttpClient instance is provided by the browser. The entire GetTestSuites method could be replaced with a single await _httpClient.GetFromJsonAsync<TestSuiteModel>(url);, where _httpClient can be an HttpClient registered through AddHttpClient. Everything else is the default behavior of GetFromJsonAsync:
public interface ITestSuiteService
{
Task<TestSuiteModel> GetTestSuites(string url);
}
public class TestSuiteService:ITestSuiteService
{
public TestSuiteService(HttpClient client)
{
_httpClient=client;
}
public async Task<TestSuiteModel> GetTestSuites(string url)
{
var model=await _httpClient.GetFromJsonAsync<TestSuiteModel>(url);
return model;
}
}
This service can be registered as a typed HttpClient :
builder.Services.AddHttpClient<ITestSuiteService,TestSuiteService>(client=>{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes(
$":{PersonalAccessToken}")
)
)
);
});
At this point the token can be retrieved from Configuration.
Another option is to use GetAsyn, inspect the response and use HttpContent.ReadFromJsonAsync. This avoids the cost of exceptions, especially in APIs when responses like 429 (Too Many Requests) are expected :
public async Task<TestSuiteModel> GetTestSuites(string url)
{
var response=await _httpClient.GetAsync(url);
if(response.IsSuccessStatusCode)
{
var model=await response.Content.ReadFromJsonAsync<TestSuiteModel>();
return model;
}
else
{
...
}
}
The initial state of your object is null.
You have do do a null check here or provide a default object. You have three ways to fix this.
1: Check for null using if
<b>
#if(TestSuitesResult is not null)
{
#TestSuitesResult.count;
}
</b>
2: Check for null using the Elvis operator
<b>
#TestSuitesResult?.count;
</b>
3: Provide a default value
TestSuiteModel TestSuitesResult = new TestSuiteModel();
Also make sure to await your function in OnInitializedAsync otherwise you are calling your function and forgetting it.
In blazor 6 you should always check if your object value is null, because the page is ALWAYS prerendered before the lyfeevents execute.
Is the only way.
Is not a problem of your applicattion , is just a Blazor bug.
This error is being solved in .net7 with #bind-after new tag.
https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-7-preview-7/
I implemented web api 2, authentication filter, based in this link https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/authentication-filters.
The filter works, but I can't apply in on Controller? I can only apply it globally like this;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new MyAuthenticationFilter()); // Global level
MyAuthenticationFilter implementation
using Test1.Web.Areas.Api.Models;
using Test1.Web.Areas.Api.Provisioning;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
public class MyAuthenticationFilter : IAuthenticationFilter
{
private static CustomerService = new CustomerService();
public bool AllowMultiple => true;
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
this.SetContextErrorResult(context);
return;
}
string apiKey = authorization.Scheme;
// 3. If there are credentials, check Schema exists. Schema has tapiKey value.
// Authorization: apiKey
if (string.IsNullOrWhiteSpace(apiKey))
{
this.SetContextErrorResult(context);
return;
}
// 4. Validate tenant. Here we could use caching
CustomerModel customer = CustomerService.Find(apiKey);
if (customer == null)
{
this.SetContextErrorResult(context);
return;
}
// 5. Credentials ok, set principal
IPrincipal principal = new GenericPrincipal(new GenericIdentity(apiKey), new string[] { });
context.Principal = principal;
return;
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
// currently we don't need authentication challenge
return;
}
private void SetContextErrorResult(HttpAuthenticationContext context)
{
context.ErrorResult = new AuthenticationFailedResponse();
}
}
public class AuthenticationFailedResponse : IHttpActionResult
{
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent(JsonConvert.SerializeObject(new ApiErrorModel()
{
Message = "Authentication failed",
Description = "Missing or incorrect credentials"
}), Encoding.UTF8, "application/json")
};
return response;
}
}
A colleague of mine found the solution:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthenticationFilter : FilterAttribute, IAuthenticationFilter
Upper code has to be added to MyAuthentication. And now we can use it on Controller:
[ApiExceptionFilter]
[RoutePrefix("api/provisioning/v0")]
[MyAuthenticationFilter]
public class ProvisioningController : ApiController
First remove following code from webconfig
config.Filters.Add(new MyAuthenticationFilter()); // Global level
Then add attrbute on controller. And first make sure you have added namespace of that auhthenticationfilter. and extend your class with ActionFilterAttribute and override method onactionexcecution.
I want to be able to login to my identity database with user name and password and retreive a JWT. Then I want to use the JWT to access data securely from my API.
I found out that the SDK code generated by VS2017 uses an old version of autorest, so I have switched to using Azure Autorest
The api and the SDK are both ASP.NET Core 2.0
To generate the SDK I use
AutoRest -mynamespace mytrack.Client -CodeGenerator CSharp -Modeler
Swagger -Input swagger.json -PackageName mytrack.client -AddCredentials true
The versions show as
AutoRest code generation utility [version: 2.0.4262; node: v8.11.2]
I have written my test as
using System;
using System.Threading.Tasks;
using Microsoft.Rest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using swagger; // my name space from the autorest command, not to be confused with swagger itself.
using swagger.Models;
namespace CoreClientTest
{
[TestClass]
public class MyTests
{
[TestMethod]
public void TestMethod1()
{
try
{
GetMyJob().Wait();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private static async Task GetMyJob()
{
var tokenRequest = new TokenRequest
{
Username = "myusername",
Password = "mypassword"
};
var credentials = new TokenCredentials("bearer token");
var uri = new Uri("https://localhost:44348", UriKind.Absolute);
var tokenClient = new Track3API(uri, credentials);
var tokenResponse = await tokenClient.ApiRequestTokenPostWithHttpMessagesAsync(tokenRequest);
var tokenContent = await tokenResponse.Response.Content.ReadAsStringAsync();
var tokenString = JObject.Parse(tokenContent).GetValue("token").ToString();
var creds2 = new TokenCredentials(tokenString);
var client2 = new Track3API(uri, creds2);
var result = await client2.ApiJobsByIdGetWithHttpMessagesAsync(1);
var response = result.Response;
Console.WriteLine(response.ToString());
}
}
}
I can see that the result has OK and I can see the token in it.
I cant see the return job
The method in the api has
[Produces("application/json")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/jobs")]
public class JobController : Controller
{
/// <summary>
/// Returns Job Header for Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}", Name = "Get")]
public IActionResult Get(int id)
{
var header1 = new JobHeader
{
JobNumber = "1234",
Id = id,
CustomerPurchaseOrderNumber = "fred"
};
return Ok(header1);
}
}
You should apply the DataContract attribute over the class so that when RestClient consumes the service reference, it also generate the types.
Read it here.
You should also attach DatamMember attribute on Property. See below example
[DataContract]
class Person
{
[DataMember]
public string Name {get; set; }
[DataMember]
public int Id {get; set; }
public Person(string name, int id)
{
this.Name = name;
this.Id = id;
}
}
When Rest Client consume the service, it will generate the classes at client side for those classes which are attributed with DataContract.
Finally it is working.
I found a tip at Andrei Dzimchuk's blog on setting up the token
using System;
using System.Threading.Tasks;
using Microsoft.Rest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using swagger;
using swagger.Models;
namespace CoreClientTest
{
[TestClass]
public class MyTests
{
[TestMethod]
public void TestMethod1()
{
try
{
GetMyJob().Wait();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
private static async Task<JobHeader> GetMyJob()
{
var tokenRequest = new TokenRequest
{
Username = "myusername",
Password = "mypassword"
};
var credentials = new TokenCredentials("bearer token");
var uri = new Uri("https://localhost:44348", UriKind.Absolute);
var tokenClient = new Track3API(uri, credentials);
var tokenResponse = await tokenClient.ApiRequestTokenPostWithHttpMessagesAsync(tokenRequest);
var tokenContent = await tokenResponse.Response.Content.ReadAsStringAsync();
var tokenString = JObject.Parse(tokenContent).GetValue("token").ToString();
var creds2 = new TokenCredentials(tokenString);
var client2 = new Track3API(uri, creds2);
var result = await client2.ApiJobsByIdGetWithHttpMessagesAsync(1);
string resultContent = await result.Response.Content.ReadAsStringAsync();
var job = JsonConvert.DeserializeObject<JobHeader>(resultContent);
Console.WriteLine(job.JobNumber);
return job;
}
}
}
I'm writing API which will be consumed by mobile devices and I want to secure this API end points.
User authentication details is provide by another application called User Manger API (another project which contains user details).
How to make use of ASP.NET Identity framework Authorization and other features to secure my API endpoints while getting the user data from the User manager API ?
The question is a bit broad; basically you are looking for a strategy to authenticate and authorise a client for a web api (dotnet core or normal framework?) using a different existing API (is that API in your control, can you modify it if needed?)
If you can modify both, id say look through StackOverflow and Google for JWT tokens, OAuth and identity server.
1- you can implement an attribute and decorate your api controller.
2- you can implement and register a global filter inside your asp.net's app_start (and make sure you are registering filters in your global.asax).
3- you can do what #Roel-Abspoel mentions implement Identity Server in your User Manager API and have your client talk to it and get the token, then your API talk to it to validate the token.
There are other ways, but i will keep this short and sweet.
Here is an example using an attribute:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Filters;
namespace myExample
{
public class ExternalAuthenticationAttribute : IAuthenticationFilter
{
public virtual bool AllowMultiple
{
get { return false; }
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// get request + authorization headers
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// check for username and password (regardless if it was validated on the client, server should check)
// this will only accept Basic Authorization
if (String.IsNullOrEmpty(authorization.Parameter) || authorization.Scheme != "Basic")
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return null;
}
var userNameAndPasword = GetCredentials(authorization.Parameter);
if (userNameAndPasword == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Could not get credentials", request);
return null;
}
// now that we have the username + password call User manager API
var client = new HttpClient();
// POST USERNAME + PASSWORD INSIDE BODY, not header, not query string. ALSO USE HTTPS to make sure it is sent encrypted
var response = AuthenticateAgainstUserMapagerApi1(userNameAndPasword, client);
// THIS WILL WORK IN .NET CORE 1.1. ALSO USE HTTPS to make sure it is sent encrypted
//var response = AuthenticateAgainstUserMapagerApi2(client, userNameAndPasword);
// parse response
if (!response.IsSuccessStatusCode)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
var readTask = response.Content.ReadAsStringAsync();
var content = readTask.Result;
context.Principal = GetPrincipal(content); // if User manager API returns a user principal as JSON we would
}
return null;
}
//private static HttpResponseMessage AuthenticateAgainstUserMapagerApi2(HttpClient client, Tuple<string, string> userNameAndPasword)
//{
// client.SetBasicAuthentication(userNameAndPasword.Item1, userNameAndPasword.Item2);
// var responseTask = client.GetAsync("https://your_user_manager_api_URL/api/authenticate");
// return responseTask.Result;
//}
private static HttpResponseMessage AuthenticateAgainstUserMapagerApi1(Tuple<string, string> userNameAndPasword, HttpClient client)
{
var credentials = new
{
Username = userNameAndPasword.Item1,
Password = userNameAndPasword.Item2
};
var responseTask = client.PostAsJsonAsync("https://your_user_manager_api_URL/api/authenticate", credentials);
var response = responseTask.Result;
return response;
}
public IPrincipal GetPrincipal(string principalStr)
{
// deserialize principalStr and return a proper Principal instead of ClaimsPrincipal below
return new ClaimsPrincipal();
}
private static Tuple<string, string> GetCredentials(string authorizationParameter)
{
byte[] credentialBytes;
try
{
credentialBytes = Convert.FromBase64String(authorizationParameter);
}
catch (FormatException)
{
return null;
}
try
{
// make sure you use the proper encoding which match client
var encoding = Encoding.ASCII;
string decodedCredentials;
decodedCredentials = encoding.GetString(credentialBytes);
int colonIndex = decodedCredentials.IndexOf(':');
string userName = decodedCredentials.Substring(0, colonIndex);
string password = decodedCredentials.Substring(colonIndex + 1);
return new Tuple<string, string>(userName, password);
}
catch (Exception ex)
{
return null;
}
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
}
use the attribute on your API class like this, which will call User Manager API each time PurchaseController is accessed:
[ExternalAuthenticationAttribute]
public class PurchaseController : ApiController
I am using this test method (and helper class) to verify the response from an external web service:
[TestMethod]
public void WebServiceReturnsSuccessResponse()
{
using (var provider = new Provider(new Info()))
using (var result = provider.GetHttpResponseMessage())
{
Assert.IsTrue(result.IsSuccessStatusCode);
}
}
private class Info : IInfo
{
public string URL { get; set; } =
"https://notreallythe.website.com:99/service/";
public string User { get; set; } = "somename";
public string Password { get; set; } = "password1";
}
I can't get this test to pass; I always get a 500 - Internal Server Error result. I have connected via an external utility (Postman) - so the web service is up and I can connect with the url & credentials that I have.
I think the problem is in my instantiation of the HttpClient class, but I can't determine where. I am using Basic authentication:
public class Provider : IProvider, IDisposable
{
private readonly HttpClient _httpClient;
public Provider(IInfo config){
if (config == null)
throw new ArgumentNullException(nameof(config));
var userInfo = new UTF8Encoding().GetBytes($"{config.User}:{config.Password}");
_httpClient = new HttpClient
{
BaseAddress = new Uri(config.URL),
DefaultRequestHeaders =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/xml")},
Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String(userInfo)),
ExpectContinue = false,
},
};
}
public HttpResponseMessage GetHttpResponseMessage()
{
return _httpClient.GetAsync("1234").Result;
}
}
The response I get back appears to go to the correct endpoint; the RequestUri in the response looks exactly like I expect, https://notreallythe.website.com:99/service/1234.
You need to load up Fiddler and do a recording of the HTTP traffic when this operation succeeds (through the browser).
Then, load up your code, stand up another instance (or window) of Fiddler, and do the same thing with your code. Now, compare the two Fiddler windows to see what is different.
You only need to compare those things in Fiddler that are highlighted in blue. You can ignore the other communications.