#859 Discriminate between providers when sharing an auth cookie
This commit is contained in:
parent
aa1fd5d89a
commit
25f39dd0f5
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
private const string CorrelationPrefix = ".AspNetCore.Correlation.";
|
||||
private const string CorrelationProperty = ".xsrf";
|
||||
private const string CorrelationMarker = "N";
|
||||
private const string AuthSchemeKey = ".AuthScheme";
|
||||
|
||||
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
|
||||
|
||||
|
|
@ -86,6 +87,9 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
// REVIEW: is this safe or good?
|
||||
ticket.Properties.RedirectUri = null;
|
||||
|
||||
// Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync
|
||||
context.Properties.Items[AuthSchemeKey] = Options.AuthenticationScheme;
|
||||
|
||||
await Options.Events.TicketReceived(context);
|
||||
|
||||
if (context.HandledResponse)
|
||||
|
|
@ -132,7 +136,11 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
return AuthenticateResult.Fail(authenticateContext.Error);
|
||||
}
|
||||
|
||||
if (authenticateContext.Principal != null)
|
||||
// The SignInScheme may be shared with multiple providers, make sure this middleware issued the identity.
|
||||
string authenticatedScheme;
|
||||
if (authenticateContext.Principal != null && authenticateContext.Properties != null
|
||||
&& authenticateContext.Properties.TryGetValue(AuthSchemeKey, out authenticatedScheme)
|
||||
&& string.Equals(Options.AuthenticationScheme, authenticatedScheme, StringComparison.Ordinal))
|
||||
{
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(authenticateContext.Principal,
|
||||
new AuthenticationProperties(authenticateContext.Properties), Options.AuthenticationScheme));
|
||||
|
|
@ -143,7 +151,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
|
||||
}
|
||||
|
||||
return AuthenticateResult.Fail("Remote authentication does not support authenticate");
|
||||
return AuthenticateResult.Fail("Remote authentication does not directly support authenticate");
|
||||
}
|
||||
|
||||
protected override Task HandleSignOutAsync(SignOutContext context)
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateWillFail()
|
||||
public async Task AuthenticateWithoutCookieWillFail()
|
||||
{
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
|
|
@ -755,6 +755,243 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
transaction.Response.Headers.GetValues("Location").First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateAutomaticWhenAlreadySignedInSucceeds()
|
||||
{
|
||||
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
ClientId = "Test Id",
|
||||
ClientSecret = "Test Secret",
|
||||
SaveTokens = true,
|
||||
StateDataFormat = stateFormat,
|
||||
BackchannelHttpHandler = CreateBackchannel()
|
||||
});
|
||||
|
||||
// Skip the challenge step, go directly to the callback path
|
||||
|
||||
var properties = new AuthenticationProperties();
|
||||
var correlationKey = ".xsrf";
|
||||
var correlationValue = "TestCorrelationId";
|
||||
properties.Items.Add(correlationKey, correlationValue);
|
||||
properties.RedirectUri = "/me";
|
||||
var state = stateFormat.Protect(properties);
|
||||
var transaction = await server.SendAsync(
|
||||
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
|
||||
$".AspNetCore.Correlation.Google.{correlationValue}=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
|
||||
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
|
||||
var authCookie = transaction.AuthenticationCookieValue;
|
||||
transaction = await server.SendAsync("https://example.com/authenticate", authCookie);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
|
||||
Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name));
|
||||
Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier));
|
||||
Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName));
|
||||
Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname));
|
||||
Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email));
|
||||
|
||||
// Ensure claims transformation
|
||||
Assert.Equal("yup", transaction.FindClaimValue("xform"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateGoogleWhenAlreadySignedInSucceeds()
|
||||
{
|
||||
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
ClientId = "Test Id",
|
||||
ClientSecret = "Test Secret",
|
||||
SaveTokens = true,
|
||||
StateDataFormat = stateFormat,
|
||||
BackchannelHttpHandler = CreateBackchannel()
|
||||
});
|
||||
|
||||
// Skip the challenge step, go directly to the callback path
|
||||
|
||||
var properties = new AuthenticationProperties();
|
||||
var correlationKey = ".xsrf";
|
||||
var correlationValue = "TestCorrelationId";
|
||||
properties.Items.Add(correlationKey, correlationValue);
|
||||
properties.RedirectUri = "/me";
|
||||
var state = stateFormat.Protect(properties);
|
||||
var transaction = await server.SendAsync(
|
||||
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
|
||||
$".AspNetCore.Correlation.Google.{correlationValue}=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
|
||||
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
|
||||
var authCookie = transaction.AuthenticationCookieValue;
|
||||
transaction = await server.SendAsync("https://example.com/authenticateGoogle", authCookie);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
|
||||
Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name));
|
||||
Assert.Equal("Test User ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier));
|
||||
Assert.Equal("Test Given Name", transaction.FindClaimValue(ClaimTypes.GivenName));
|
||||
Assert.Equal("Test Family Name", transaction.FindClaimValue(ClaimTypes.Surname));
|
||||
Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email));
|
||||
|
||||
// Ensure claims transformation
|
||||
Assert.Equal("yup", transaction.FindClaimValue("xform"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChallengeGoogleWhenAlreadySignedInReturnsForbidden()
|
||||
{
|
||||
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
ClientId = "Test Id",
|
||||
ClientSecret = "Test Secret",
|
||||
SaveTokens = true,
|
||||
StateDataFormat = stateFormat,
|
||||
BackchannelHttpHandler = CreateBackchannel()
|
||||
});
|
||||
|
||||
// Skip the challenge step, go directly to the callback path
|
||||
|
||||
var properties = new AuthenticationProperties();
|
||||
var correlationKey = ".xsrf";
|
||||
var correlationValue = "TestCorrelationId";
|
||||
properties.Items.Add(correlationKey, correlationValue);
|
||||
properties.RedirectUri = "/me";
|
||||
var state = stateFormat.Protect(properties);
|
||||
var transaction = await server.SendAsync(
|
||||
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
|
||||
$".AspNetCore.Correlation.Google.{correlationValue}=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
|
||||
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
|
||||
var authCookie = transaction.AuthenticationCookieValue;
|
||||
transaction = await server.SendAsync("https://example.com/challenge", authCookie);
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.StartsWith("https://example.com/Account/AccessDenied?", transaction.Response.Headers.Location.OriginalString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateFacebookWhenAlreadySignedWithGoogleReturnsNull()
|
||||
{
|
||||
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
ClientId = "Test Id",
|
||||
ClientSecret = "Test Secret",
|
||||
SaveTokens = true,
|
||||
StateDataFormat = stateFormat,
|
||||
BackchannelHttpHandler = CreateBackchannel()
|
||||
});
|
||||
|
||||
// Skip the challenge step, go directly to the callback path
|
||||
|
||||
var properties = new AuthenticationProperties();
|
||||
var correlationKey = ".xsrf";
|
||||
var correlationValue = "TestCorrelationId";
|
||||
properties.Items.Add(correlationKey, correlationValue);
|
||||
properties.RedirectUri = "/me";
|
||||
var state = stateFormat.Protect(properties);
|
||||
var transaction = await server.SendAsync(
|
||||
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
|
||||
$".AspNetCore.Correlation.Google.{correlationValue}=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
|
||||
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
|
||||
var authCookie = transaction.AuthenticationCookieValue;
|
||||
transaction = await server.SendAsync("https://example.com/authenticateFacebook", authCookie);
|
||||
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
|
||||
Assert.Equal(null, transaction.FindClaimValue(ClaimTypes.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChallengeFacebookWhenAlreadySignedWithGoogleSucceeds()
|
||||
{
|
||||
var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider().CreateProtector("GoogleTest"));
|
||||
var server = CreateServer(new GoogleOptions
|
||||
{
|
||||
ClientId = "Test Id",
|
||||
ClientSecret = "Test Secret",
|
||||
SaveTokens = true,
|
||||
StateDataFormat = stateFormat,
|
||||
BackchannelHttpHandler = CreateBackchannel()
|
||||
});
|
||||
|
||||
// Skip the challenge step, go directly to the callback path
|
||||
|
||||
var properties = new AuthenticationProperties();
|
||||
var correlationKey = ".xsrf";
|
||||
var correlationValue = "TestCorrelationId";
|
||||
properties.Items.Add(correlationKey, correlationValue);
|
||||
properties.RedirectUri = "/me";
|
||||
var state = stateFormat.Protect(properties);
|
||||
var transaction = await server.SendAsync(
|
||||
"https://example.com/signin-google?code=TestCode&state=" + UrlEncoder.Default.Encode(state),
|
||||
$".AspNetCore.Correlation.Google.{correlationValue}=N");
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
|
||||
Assert.Equal(2, transaction.SetCookie.Count);
|
||||
Assert.Contains($".AspNetCore.Correlation.Google.{correlationValue}", transaction.SetCookie[0]); // Delete
|
||||
Assert.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme, transaction.SetCookie[1]);
|
||||
|
||||
var authCookie = transaction.AuthenticationCookieValue;
|
||||
transaction = await server.SendAsync("https://example.com/challengeFacebook", authCookie);
|
||||
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
|
||||
Assert.StartsWith("https://www.facebook.com/", transaction.Response.Headers.Location.OriginalString);
|
||||
}
|
||||
|
||||
private HttpMessageHandler CreateBackchannel()
|
||||
{
|
||||
return new TestHttpMessageHandler()
|
||||
{
|
||||
Sender = req =>
|
||||
{
|
||||
if (req.RequestUri.AbsoluteUri == "https://www.googleapis.com/oauth2/v4/token")
|
||||
{
|
||||
return ReturnJsonResponse(new
|
||||
{
|
||||
access_token = "Test Access Token",
|
||||
expires_in = 3600,
|
||||
token_type = "Bearer"
|
||||
});
|
||||
}
|
||||
else if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://www.googleapis.com/plus/v1/people/me")
|
||||
{
|
||||
return ReturnJsonResponse(new
|
||||
{
|
||||
id = "Test User ID",
|
||||
displayName = "Test Name",
|
||||
name = new
|
||||
{
|
||||
familyName = "Test Family Name",
|
||||
givenName = "Test Given Name"
|
||||
},
|
||||
url = "Profile link",
|
||||
emails = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
value = "Test email",
|
||||
type = "account"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw new NotImplementedException(req.RequestUri.AbsoluteUri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK)
|
||||
{
|
||||
var res = new HttpResponseMessage(code);
|
||||
|
|
@ -774,6 +1011,11 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
AutomaticAuthenticate = true
|
||||
});
|
||||
app.UseGoogleAuthentication(options);
|
||||
app.UseFacebookAuthentication(new FacebookOptions()
|
||||
{
|
||||
AppId = "Test AppId",
|
||||
AppSecret = "Test AppSecrent",
|
||||
});
|
||||
app.UseClaimsTransformation(context =>
|
||||
{
|
||||
var id = new ClaimsIdentity("xform");
|
||||
|
|
@ -789,6 +1031,10 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
{
|
||||
await context.Authentication.ChallengeAsync("Google");
|
||||
}
|
||||
else if (req.Path == new PathString("/challengeFacebook"))
|
||||
{
|
||||
await context.Authentication.ChallengeAsync("Facebook");
|
||||
}
|
||||
else if (req.Path == new PathString("/tokens"))
|
||||
{
|
||||
var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
|
||||
|
|
@ -800,6 +1046,21 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
|||
{
|
||||
res.Describe(context.User);
|
||||
}
|
||||
else if (req.Path == new PathString("/authenticate"))
|
||||
{
|
||||
var user = await context.Authentication.AuthenticateAsync(Http.Authentication.AuthenticationManager.AutomaticScheme);
|
||||
res.Describe(user);
|
||||
}
|
||||
else if (req.Path == new PathString("/authenticateGoogle"))
|
||||
{
|
||||
var user = await context.Authentication.AuthenticateAsync("Google");
|
||||
res.Describe(user);
|
||||
}
|
||||
else if (req.Path == new PathString("/authenticateFacebook"))
|
||||
{
|
||||
var user = await context.Authentication.AuthenticateAsync("Facebook");
|
||||
res.Describe(user);
|
||||
}
|
||||
else if (req.Path == new PathString("/unauthorized"))
|
||||
{
|
||||
// Simulate Authorization failure
|
||||
|
|
|
|||
Loading…
Reference in New Issue