2

Get ASP.NET MVC5 WebAPI token fails sometimes

Code

string GetAPITokenSync(string username, string password, string apiBaseUri)
        {
            var token = string.Empty;

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(apiBaseUri);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.Timeout = TimeSpan.FromSeconds(60);  

                //setup login data
                var formContent = new FormUrlEncodedContent(new[]
                {
                 new KeyValuePair<string, string>("grant_type", "password"),
                 new KeyValuePair<string, string>("username", username),
                 new KeyValuePair<string, string>("password", password),
                 });

                //send request               
                Task t = Task.Run(() =>
                {
                    HttpResponseMessage responseMessage = client.PostAsync("/Token", formContent).Result;
                    var responseJson = responseMessage.Content.ReadAsStringAsync().Result;
                    var jObject = JObject.Parse(responseJson);
                    token = jObject.GetValue("access_token").ToString();
                });

                t.Wait();
                t.Dispose();
                t = null;
                GC.Collect();

                return token;
            }
        }

Error

One or more errors occurred. ---> System.AggregateException: One or more errors occurred. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
--- End of inner exception stack trace --- at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceled Exceptions) at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1.get_Result()

WebAPi login method is by default has no changes.

[HttpPost]
[AllowAnonymous]
[Route("Login")]
public HttpResponseMessage Login(string username, string password)
    {
        try
        {
            var identityUser = UserManager.Find(username, password);

            if (identityUser != null)
            {
                var identity = new ClaimsIdentity(Startup.OAuthOptions.AuthenticationType);
                identity.AddClaim(new Claim(ClaimTypes.Name, username));

                AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties());
                var currentUtc = new SystemClock().UtcNow;
                ticket.Properties.IssuedUtc = currentUtc;
                ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(1440));

                var token = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);

                var response = new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new ObjectContent<object>(new
                    {
                        UserName = username,
                        ExternalAccessToken = token
                    }, Configuration.Formatters.JsonFormatter)
                };

                return response;


            }
        }
        catch (Exception)
        {
        }

        return new HttpResponseMessage(HttpStatusCode.BadRequest);
    }
}

Startup class is by default no changes

 public partial class Startup
    {
        public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

        public static string PublicClientId { get; private set; }


        public void ConfigureAuth(IAppBuilder app)
        {
            // Configure the db context and user manager to use a single instance per request
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Configure the application for OAuth based flow
            PublicClientId = "self";
            OAuthOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/Token"),
                Provider = new ApplicationOAuthProvider(PublicClientId),
                AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
                // In production mode set AllowInsecureHttp = false
                AllowInsecureHttp = true
            };

            // Enable the application to use bearer tokens to authenticate users
            app.UseOAuthBearerTokens(OAuthOptions);
        }
    }

Any clue?

NoWar
  • 36,338
  • 80
  • 323
  • 498
  • 1
    Isn't it timing out? after 60 seconds? Why are you running the Post on a task? Then blocking your calling thread waiting for the task to finish? – Bruno Garcia Apr 29 '16 at 11:42
  • @BrunoGarcia I guess 60 seconds is ok oto get token. Please advice something to consider... Thank you! – NoWar Apr 29 '16 at 11:44
  • I mean: Isn't the problem because your call is timing out? Either because it can't reach the server or it takes too long (over 60 seconds) to complete? Specially if it fails sometimes only – Bruno Garcia Apr 29 '16 at 11:52
  • @BrunoGarcia It should work... I will try this solution as well http://stackoverflow.com/questions/30656795/web-api-bad-request-when-getting-access-token-after-moving-to-production – NoWar Apr 29 '16 at 12:10
  • @Dimi I don't think you've answered the question of whether it's a timeout. Is it? – Todd Menier Apr 29 '16 at 14:20
  • @ToddMenier I have got exactly the same problem described here http://stackoverflow.com/questions/36937328/get-asp-net-mvc5-webapi-token-fails-sometimes?noredirect=1#comment61440461_36937328 – NoWar Apr 29 '16 at 14:21
  • you just linked back to this issue. Is it a timeout - yes, no, or you don't know? – Todd Menier Apr 29 '16 at 14:30
  • @ToddMenier Well... I am facing the error that Ive included into the question is `A task was canceled.` – NoWar Apr 29 '16 at 14:32
  • @ToddMenier Another thing is that when app works in console mode no errors and when i install it like a service it appears. – NoWar Apr 29 '16 at 14:33
  • Does it take a long time before it returns that exception? – Todd Menier Apr 29 '16 at 14:36
  • @ToddMenier Not at all. I use Topshelf just to switch betwene two modes. SO in console mode no problem. – NoWar Apr 29 '16 at 14:38

1 Answers1

6

It's hard to say for certain, but the way you're blocking HttpClient calls can't be helping. HttpClient is an async-only library; you may have a deadlock situation. I suggest getting rid of all .Results and .Wait()s and write everything asynchronously, using async/await. And your Task.Run is achieving nothing, so that should go.

I understand this is Topshelf app ported over from a console app. I'm not very familiar with Topshelf, but I assume that, like console apps, you need to block somewhere or your app will simply exit. The place to do that is at the very top - the entry point of the app.

This demonstrates the pattern, along with a re-write of your GetApiToken method:

// app entry point - the only place you should block
void Main()
{
    MainAsync().Wait();
}

// the "real" starting point of your app logic. do everything async from here on
async Task MainAsync()
{
    ...
    var token = await GetApiTokenAsync(username, password, apiBaseUri);
    ...
}

async Task<string> GetApiTokenAsync(string username, string password, string apiBaseUri)
{
    var token = string.Empty;

    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri(apiBaseUri);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.Timeout = TimeSpan.FromSeconds(60);  

        //setup login data
        var formContent = new FormUrlEncodedContent(new[]
        {
         new KeyValuePair<string, string>("grant_type", "password"),
         new KeyValuePair<string, string>("username", username),
         new KeyValuePair<string, string>("password", password),
         });

        //send request               
        HttpResponseMessage responseMessage = await client.PostAsync("/Token", formContent);
        var responseJson = await responseMessage.Content.ReadAsStringAsync();
        var jObject = JObject.Parse(responseJson);
        token = jObject.GetValue("access_token").ToString();

        return token;
    }
}
Todd Menier
  • 37,557
  • 17
  • 150
  • 173