Further improve error handling for OAuth providers

This commit is contained in:
Hao Kung 2015-11-03 12:37:35 -08:00
parent e9d2c53ebc
commit 238fdf24e8
6 changed files with 80 additions and 46 deletions

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Authentication.Facebook
} }
// The refresh token is not available. // The refresh token is not available.
return new OAuthTokenResponse(payload); return OAuthTokenResponse.Success(payload);
} }
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)

View File

@ -7,6 +7,7 @@ using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Http.Authentication;
@ -62,9 +63,14 @@ namespace Microsoft.AspNet.Authentication.OAuth
var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath)); var tokens = await ExchangeCodeAsync(code, BuildRedirectUri(Options.CallbackPath));
if (tokens.Error != null)
{
return AuthenticateResult.Failed(tokens.Error);
}
if (string.IsNullOrEmpty(tokens.AccessToken)) if (string.IsNullOrEmpty(tokens.AccessToken))
{ {
return AuthenticateResult.Failed("Access token was not found."); return AuthenticateResult.Failed("Failed to retrieve access token.");
} }
var identity = new ClaimsIdentity(Options.ClaimsIssuer); var identity = new ClaimsIdentity(Options.ClaimsIssuer);
@ -113,9 +119,25 @@ namespace Microsoft.AspNet.Authentication.OAuth
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
requestMessage.Content = requestContent; requestMessage.Content = requestContent;
var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
response.EnsureSuccessStatusCode(); if (response.IsSuccessStatusCode)
var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); {
return new OAuthTokenResponse(payload); var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
return OAuthTokenResponse.Success(payload);
}
else
{
var error = "OAuth token endpoint failure: " + await Display(response);
return OAuthTokenResponse.Failed(new Exception(error));
}
}
private static async Task<string> Display(HttpResponseMessage response)
{
var output = new StringBuilder();
output.Append("Status: " + response.StatusCode + ";");
output.Append("Headers: " + response.Headers.ToString() + ";");
output.Append("Body: " + await response.Content.ReadAsStringAsync() + ";");
return output.ToString();
} }
protected virtual async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) protected virtual async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)

View File

@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Authentication.OAuth namespace Microsoft.AspNet.Authentication.OAuth
{ {
public class OAuthTokenResponse public class OAuthTokenResponse
{ {
public OAuthTokenResponse(JObject response) private OAuthTokenResponse(JObject response)
{ {
Response = response; Response = response;
AccessToken = response.Value<string>("access_token"); AccessToken = response.Value<string>("access_token");
@ -16,10 +17,26 @@ namespace Microsoft.AspNet.Authentication.OAuth
ExpiresIn = response.Value<string>("expires_in"); ExpiresIn = response.Value<string>("expires_in");
} }
private OAuthTokenResponse(Exception error)
{
Error = error;
}
public static OAuthTokenResponse Success(JObject response)
{
return new OAuthTokenResponse(response);
}
public static OAuthTokenResponse Failed(Exception error)
{
return new OAuthTokenResponse(error);
}
public JObject Response { get; set; } public JObject Response { get; set; }
public string AccessToken { get; set; } public string AccessToken { get; set; }
public string TokenType { get; set; } public string TokenType { get; set; }
public string RefreshToken { get; set; } public string RefreshToken { get; set; }
public string ExpiresIn { get; set; } public string ExpiresIn { get; set; }
public Exception Error { get; set; }
} }
} }

View File

@ -36,8 +36,7 @@ namespace Microsoft.AspNet.Authentication
return false; return false;
} }
Context.Response.StatusCode = 500; throw new AggregateException("Unhandled remote error.", errorContext.Error);
return true;
} }
// We have a ticket if we get here // We have a ticket if we get here

View File

@ -244,8 +244,8 @@ namespace Microsoft.AspNet.Authentication.Google
options.ClientId = "Test Id"; options.ClientId = "Test Id";
options.ClientSecret = "Test Secret"; options.ClientSecret = "Test Secret";
}); });
var transaction = await server.SendAsync("https://example.com/signin-google?code=TestCode"); var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-google?code=TestCode"));
Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); Assert.Equal("The oauth state was missing or invalid.", error.Message);
} }
[Theory] [Theory]
@ -270,15 +270,16 @@ namespace Microsoft.AspNet.Authentication.Google
}; };
} }
}); });
var transaction = await server.SendAsync("https://example.com/signin-google?error=OMG");
if (redirect) if (redirect)
{ {
var transaction = await server.SendAsync("https://example.com/signin-google?error=OMG");
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/error?ErrorMessage=OMG", transaction.Response.Headers.GetValues("Location").First()); Assert.Equal("/error?ErrorMessage=OMG", transaction.Response.Headers.GetValues("Location").First());
} }
else else
{ {
Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-google?error=OMG"));
Assert.Equal("OMG", error.Message);
} }
} }
@ -379,7 +380,8 @@ namespace Microsoft.AspNet.Authentication.Google
{ {
Sender = req => Sender = req =>
{ {
return new HttpResponseMessage(HttpStatusCode.BadRequest); return ReturnJsonResponse(new { Error = "Error" },
HttpStatusCode.BadRequest);
} }
}; };
if (redirect) if (redirect)
@ -402,24 +404,21 @@ namespace Microsoft.AspNet.Authentication.Google
properties.RedirectUri = "/me"; properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties); var state = stateFormat.Protect(properties);
var sendTask = server.SendAsync(
await Assert.ThrowsAsync<HttpRequestException>(() => server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue)); correlationKey + "=" + correlationValue);
if (redirect)
//var transaction = await server.SendAsync( {
// "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), var transaction = await sendTask;
// correlationKey + "=" + correlationValue); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
//if (redirect) Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"),
//{ transaction.Response.Headers.GetValues("Location").First());
// Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); }
// Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("Access token was not found."), else
// transaction.Response.Headers.GetValues("Location").First()); {
//} var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
//else Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", error.Message);
//{ }
// Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode);
//}
} }
[Theory] [Theory]
@ -459,18 +458,20 @@ namespace Microsoft.AspNet.Authentication.Google
properties.Items.Add(correlationKey, correlationValue); properties.Items.Add(correlationKey, correlationValue);
properties.RedirectUri = "/me"; properties.RedirectUri = "/me";
var state = stateFormat.Protect(properties); var state = stateFormat.Protect(properties);
var transaction = await server.SendAsync( var sendTask = server.SendAsync(
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state), "https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
correlationKey + "=" + correlationValue); correlationKey + "=" + correlationValue);
if (redirect) if (redirect)
{ {
var transaction = await sendTask;
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("Access token was not found."), Assert.Equal("/error?ErrorMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."),
transaction.Response.Headers.GetValues("Location").First()); transaction.Response.Headers.GetValues("Location").First());
} }
else else
{ {
Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode); var error = await Assert.ThrowsAnyAsync<Exception>(() => sendTask);
Assert.Equal("Failed to retrieve access token.", error.Message);
} }
} }
@ -712,9 +713,8 @@ namespace Microsoft.AspNet.Authentication.Google
} }
[Fact] [Fact]
public async Task NoStateCauses500() public async Task NoStateCausesException()
{ {
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
var server = CreateServer(options => var server = CreateServer(options =>
{ {
options.ClientId = "Test Id"; options.ClientId = "Test Id";
@ -722,10 +722,8 @@ namespace Microsoft.AspNet.Authentication.Google
}); });
//Post a message to the Google middleware //Post a message to the Google middleware
var transaction = await server.SendAsync( var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-google?code=TestCode"));
"https://example.com/signin-google?code=TestCode"); Assert.Equal("The oauth state was missing or invalid.", error.Message);
Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode);
} }
[Fact] [Fact]
@ -756,9 +754,9 @@ namespace Microsoft.AspNet.Authentication.Google
transaction.Response.Headers.GetValues("Location").First()); transaction.Response.Headers.GetValues("Location").First());
} }
private static HttpResponseMessage ReturnJsonResponse(object content) private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK)
{ {
var res = new HttpResponseMessage(HttpStatusCode.OK); var res = new HttpResponseMessage(code);
var text = JsonConvert.SerializeObject(content); var text = JsonConvert.SerializeObject(content);
res.Content = new StringContent(text, Encoding.UTF8, "application/json"); res.Content = new StringContent(text, Encoding.UTF8, "application/json");
return res; return res;

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
} }
[Fact] [Fact]
public async Task BadSignInWill500() public async Task BadSignInWillThrow()
{ {
var server = CreateServer(options => var server = CreateServer(options =>
{ {
@ -73,10 +73,8 @@ namespace Microsoft.AspNet.Authentication.Twitter
}); });
// Send a bogus sign in // Send a bogus sign in
var transaction = await server.SendAsync( var error = await Assert.ThrowsAnyAsync<Exception>(() => server.SendAsync("https://example.com/signin-twitter"));
"https://example.com/signin-twitter"); Assert.Equal("Invalid state cookie.", error.Message);
Assert.Equal(HttpStatusCode.InternalServerError, transaction.Response.StatusCode);
} }
[Fact] [Fact]