Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

Commit

Permalink
#859 Discriminate between providers when sharing an auth cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher committed Sep 20, 2016
1 parent 22d2fe9 commit cbd003a
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHand
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();

Expand Down Expand Up @@ -86,6 +87,9 @@ protected virtual async Task<bool> HandleRemoteCallbackAsync()
// 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)
Expand Down Expand Up @@ -132,7 +136,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
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));
Expand All @@ -143,7 +151,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()

}

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public async Task ChallengeWillTriggerApplyRedirectEvent()
}

[Fact]
public async Task AuthenticateWillFail()
public async Task AuthenticateWithoutCookieWillFail()
{
var server = CreateServer(new GoogleOptions
{
Expand Down Expand Up @@ -755,6 +755,243 @@ public async Task CanRedirectOnError()
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);
Expand All @@ -774,6 +1011,11 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext,
AutomaticAuthenticate = true
});
app.UseGoogleAuthentication(options);
app.UseFacebookAuthentication(new FacebookOptions()
{
AppId = "Test AppId",
AppSecret = "Test AppSecrent",
});
app.UseClaimsTransformation(context =>
{
var id = new ClaimsIdentity("xform");
Expand All @@ -789,6 +1031,10 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext,
{
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);
Expand All @@ -800,6 +1046,21 @@ private static TestServer CreateServer(GoogleOptions options, Func<HttpContext,
{
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
Expand Down

0 comments on commit cbd003a

Please sign in to comment.