#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 CorrelationPrefix = ".AspNetCore.Correlation.";
|
||||||
private const string CorrelationProperty = ".xsrf";
|
private const string CorrelationProperty = ".xsrf";
|
||||||
private const string CorrelationMarker = "N";
|
private const string CorrelationMarker = "N";
|
||||||
|
private const string AuthSchemeKey = ".AuthScheme";
|
||||||
|
|
||||||
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
|
private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
|
||||||
|
|
||||||
|
|
@ -86,6 +87,9 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
// REVIEW: is this safe or good?
|
// REVIEW: is this safe or good?
|
||||||
ticket.Properties.RedirectUri = null;
|
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);
|
await Options.Events.TicketReceived(context);
|
||||||
|
|
||||||
if (context.HandledResponse)
|
if (context.HandledResponse)
|
||||||
|
|
@ -132,7 +136,11 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
return AuthenticateResult.Fail(authenticateContext.Error);
|
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,
|
return AuthenticateResult.Success(new AuthenticationTicket(authenticateContext.Principal,
|
||||||
new AuthenticationProperties(authenticateContext.Properties), Options.AuthenticationScheme));
|
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)
|
protected override Task HandleSignOutAsync(SignOutContext context)
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AuthenticateWillFail()
|
public async Task AuthenticateWithoutCookieWillFail()
|
||||||
{
|
{
|
||||||
var server = CreateServer(new GoogleOptions
|
var server = CreateServer(new GoogleOptions
|
||||||
{
|
{
|
||||||
|
|
@ -755,6 +755,243 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
||||||
transaction.Response.Headers.GetValues("Location").First());
|
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)
|
private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK)
|
||||||
{
|
{
|
||||||
var res = new HttpResponseMessage(code);
|
var res = new HttpResponseMessage(code);
|
||||||
|
|
@ -774,6 +1011,11 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
||||||
AutomaticAuthenticate = true
|
AutomaticAuthenticate = true
|
||||||
});
|
});
|
||||||
app.UseGoogleAuthentication(options);
|
app.UseGoogleAuthentication(options);
|
||||||
|
app.UseFacebookAuthentication(new FacebookOptions()
|
||||||
|
{
|
||||||
|
AppId = "Test AppId",
|
||||||
|
AppSecret = "Test AppSecrent",
|
||||||
|
});
|
||||||
app.UseClaimsTransformation(context =>
|
app.UseClaimsTransformation(context =>
|
||||||
{
|
{
|
||||||
var id = new ClaimsIdentity("xform");
|
var id = new ClaimsIdentity("xform");
|
||||||
|
|
@ -789,6 +1031,10 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
||||||
{
|
{
|
||||||
await context.Authentication.ChallengeAsync("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"))
|
else if (req.Path == new PathString("/tokens"))
|
||||||
{
|
{
|
||||||
var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
|
var authContext = new AuthenticateContext(TestExtensions.CookieAuthenticationScheme);
|
||||||
|
|
@ -800,6 +1046,21 @@ namespace Microsoft.AspNetCore.Authentication.Google
|
||||||
{
|
{
|
||||||
res.Describe(context.User);
|
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"))
|
else if (req.Path == new PathString("/unauthorized"))
|
||||||
{
|
{
|
||||||
// Simulate Authorization failure
|
// Simulate Authorization failure
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue