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

Commit 7b0dca3

Browse files
committed
#1443 Block unsolicited wsfed logins by default.
1 parent 38f726d commit 7b0dca3

File tree

3 files changed

+142
-104
lines changed

3 files changed

+142
-104
lines changed

src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationHandler.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Authentication.WsFederation
2020
/// </summary>
2121
public class WsFederationHandler : RemoteAuthenticationHandler<WsFederationOptions>, IAuthenticationSignOutHandler
2222
{
23+
private const string CorrelationProperty = ".xsrf";
2324
private WsFederationConfiguration _configuration;
2425

2526
/// <summary>
@@ -78,6 +79,8 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
7879
wsFederationMessage.Wreply = BuildRedirectUri(Options.CallbackPath);
7980
}
8081

82+
GenerateCorrelationId(properties);
83+
8184
var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
8285
{
8386
ProtocolMessage = wsFederationMessage
@@ -141,12 +144,15 @@ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync
141144
{
142145
// Retrieve our cached redirect uri
143146
var state = wsFederationMessage.Wctx;
144-
// WsFed allows for uninitiated logins, state may be missing.
147+
// WsFed allows for uninitiated logins, state may be missing. See AllowUnsolicitedLogins.
145148
var properties = Options.StateDataFormat.Unprotect(state);
146149

147150
if (properties == null)
148151
{
149-
properties = new AuthenticationProperties();
152+
if (!Options.AllowUnsolicitedLogins)
153+
{
154+
return HandleRequestResult.Fail("Unsolicited logins are not allowed.");
155+
}
150156
}
151157
else
152158
{
@@ -164,6 +170,15 @@ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync
164170
{
165171
return messageReceivedContext.Result;
166172
}
173+
wsFederationMessage = messageReceivedContext.ProtocolMessage;
174+
properties = messageReceivedContext.Properties; // Provides a new instance if not set.
175+
176+
// If state did flow from the challenge then validate it. See AllowUnsolicitedLogins above.
177+
if (properties.Items.TryGetValue(CorrelationProperty, out string correlationId)
178+
&& !ValidateCorrelationId(properties))
179+
{
180+
return HandleRequestResult.Fail("Correlation failed.");
181+
}
167182

168183
if (wsFederationMessage.Wresult == null)
169184
{
@@ -187,6 +202,8 @@ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync
187202
{
188203
return securityTokenReceivedContext.Result;
189204
}
205+
wsFederationMessage = securityTokenReceivedContext.ProtocolMessage;
206+
properties = messageReceivedContext.Properties;
190207

191208
if (_configuration == null)
192209
{

src/Microsoft.AspNetCore.Authentication.WsFederation/WsFederationOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,11 @@ public TokenValidationParameters TokenValidationParameters
153153
/// The default is true. This should be disabled only in development environments.
154154
/// </summary>
155155
public bool RequireHttpsMetadata { get; set; } = true;
156+
157+
/// <summary>
158+
/// The Ws-Federation protocol allows the user to initiate logins without contacting the application for a Challenge first.
159+
/// However, that flow is susceptible to XSRF and other attacks so it is disabled here by default.
160+
/// </summary>
161+
public bool AllowUnsolicitedLogins { get; set; }
156162
}
157163
}

test/Microsoft.AspNetCore.Authentication.WsFederation.Test/WsFederationTest.cs

Lines changed: 117 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
@@ -51,53 +52,42 @@ public async Task ValidTokenIsAccepted()
5152
var response = await httpClient.GetAsync("/");
5253
var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
5354

54-
// Send an invalid token and verify that the token is not honored
55-
var kvps = new List<KeyValuePair<string, string>>();
56-
kvps.Add(new KeyValuePair<string, string>("wa", "wsignin1.0"));
57-
kvps.Add(new KeyValuePair<string, string>("wresult", File.ReadAllText(@"ValidToken.xml")));
58-
kvps.Add(new KeyValuePair<string, string>("wctx", queryItems["wctx"]));
59-
response = await httpClient.PostAsync(queryItems["wreply"], new FormUrlEncodedContent(kvps));
55+
var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]);
56+
CopyCookies(response, request);
57+
request.Content = CreateSignInContent("ValidToken.xml", queryItems["wctx"]);
58+
response = await httpClient.SendAsync(request);
6059

6160
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
6261

63-
var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
64-
var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList());
65-
foreach (var cookie in cookies)
66-
{
67-
if (cookie.Value.HasValue)
68-
{
69-
request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
70-
}
71-
}
62+
request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
63+
CopyCookies(response, request);
7264
response = await httpClient.SendAsync(request);
7365

7466
// Did the request end in the actual resource requested for
7567
Assert.Equal(WsFederationDefaults.AuthenticationScheme, await response.Content.ReadAsStringAsync());
7668
}
7769

7870
[Fact]
79-
public async Task ValidUnsolicitedTokenIsAccepted()
71+
public async Task ValidUnsolicitedTokenIsRefused()
8072
{
8173
var httpClient = CreateClient();
74+
var form = CreateSignInContent("ValidToken.xml", suppressWctx: true);
75+
var exception = await Assert.ThrowsAsync<Exception>(() => httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form));
76+
Assert.Contains("Unsolicited logins are not allowed.", exception.Message);
77+
}
8278

83-
// Send an invalid token and verify that the token is not honored
84-
var kvps = new List<KeyValuePair<string, string>>();
85-
kvps.Add(new KeyValuePair<string, string>("wa", "wsignin1.0"));
86-
kvps.Add(new KeyValuePair<string, string>("wresult", File.ReadAllText(@"ValidToken.xml")));
87-
kvps.Add(new KeyValuePair<string, string>("suppressWctx", "true"));
88-
var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", new FormUrlEncodedContent(kvps));
79+
[Fact]
80+
public async Task ValidUnsolicitedTokenIsAcceptedWhenAllowed()
81+
{
82+
var httpClient = CreateClient(allowUnsolicited: true);
83+
84+
var form = CreateSignInContent("ValidToken.xml", suppressWctx: true);
85+
var response = await httpClient.PostAsync(httpClient.BaseAddress + "signin-wsfed", form);
8986

9087
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
9188

9289
var request = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location);
93-
var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList());
94-
foreach (var cookie in cookies)
95-
{
96-
if (cookie.Value.HasValue)
97-
{
98-
request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
99-
}
100-
}
90+
CopyCookies(response, request);
10191
response = await httpClient.SendAsync(request);
10292

10393
// Did the request end in the actual resource requested for
@@ -113,94 +103,119 @@ public async Task InvalidTokenIsRejected()
113103
var response = await httpClient.GetAsync("/");
114104
var queryItems = QueryHelpers.ParseQuery(response.Headers.Location.Query);
115105

116-
// Send an invalid token and verify that the token is not honored
117-
var kvps = new List<KeyValuePair<string, string>>();
118-
kvps.Add(new KeyValuePair<string, string>("wa", "wsignin1.0"));
119-
kvps.Add(new KeyValuePair<string, string>("wresult", File.ReadAllText(@"InvalidToken.xml")));
120-
kvps.Add(new KeyValuePair<string, string>("wctx", queryItems["wctx"]));
121-
response = await httpClient.PostAsync(queryItems["wreply"], new FormUrlEncodedContent(kvps));
106+
var request = new HttpRequestMessage(HttpMethod.Post, queryItems["wreply"]);
107+
CopyCookies(response, request);
108+
request.Content = CreateSignInContent("InvalidToken.xml", queryItems["wctx"]);
109+
response = await httpClient.SendAsync(request);
122110

123111
// Did the request end in the actual resource requested for
124112
Assert.Equal("AuthenticationFailed", await response.Content.ReadAsStringAsync());
125113
}
126114

127-
private HttpClient CreateClient()
115+
private FormUrlEncodedContent CreateSignInContent(string tokenFile, string wctx = null, bool suppressWctx = false)
128116
{
129-
var builder = new WebHostBuilder()
130-
.ConfigureServices(ConfigureAppServices)
131-
.Configure(ConfigureApp);
132-
var server = new TestServer(builder);
133-
return server.CreateClient();
117+
var kvps = new List<KeyValuePair<string, string>>();
118+
kvps.Add(new KeyValuePair<string, string>("wa", "wsignin1.0"));
119+
kvps.Add(new KeyValuePair<string, string>("wresult", File.ReadAllText(tokenFile)));
120+
if (!string.IsNullOrEmpty(wctx))
121+
{
122+
kvps.Add(new KeyValuePair<string, string>("wctx", wctx));
123+
}
124+
if (suppressWctx)
125+
{
126+
kvps.Add(new KeyValuePair<string, string>("suppressWctx", "true"));
127+
}
128+
return new FormUrlEncodedContent(kvps);
134129
}
135130

136-
private void ConfigureAppServices(IServiceCollection services)
131+
private void CopyCookies(HttpResponseMessage response, HttpRequestMessage request)
137132
{
138-
services.AddAuthentication(sharedOptions =>
139-
{
140-
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
141-
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
142-
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
143-
})
144-
.AddWsFederation(options =>
133+
var cookies = SetCookieHeaderValue.ParseList(response.Headers.GetValues(HeaderNames.SetCookie).ToList());
134+
foreach (var cookie in cookies)
145135
{
146-
options.Wtrealm = "http://Automation1";
147-
options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml";
148-
options.BackchannelHttpHandler = new WaadMetadataDocumentHandler();
149-
options.StateDataFormat = new CustomStateDataFormat();
150-
options.SecurityTokenHandlers = new List<ISecurityTokenValidator>() { new TestSecurityTokenValidator() };
151-
options.UseTokenLifetime = false;
152-
options.Events = new WsFederationEvents()
136+
if (cookie.Value.HasValue)
137+
{
138+
request.Headers.Add(HeaderNames.Cookie, new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
139+
}
140+
}
141+
}
142+
143+
private HttpClient CreateClient(bool allowUnsolicited = false)
144+
{
145+
var builder = new WebHostBuilder()
146+
.Configure(ConfigureApp)
147+
.ConfigureServices(services =>
153148
{
154-
MessageReceived = context =>
149+
services.AddAuthentication(sharedOptions =>
155150
{
156-
if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress))
157-
{
158-
Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value");
159-
}
160-
context.HttpContext.Items["MessageReceived"] = true;
161-
return Task.FromResult(0);
162-
},
163-
RedirectToIdentityProvider = context =>
151+
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
152+
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
153+
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
154+
})
155+
.AddCookie()
156+
.AddWsFederation(options =>
164157
{
165-
if (context.ProtocolMessage.IsSignInMessage)
158+
options.Wtrealm = "http://Automation1";
159+
options.MetadataAddress = "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/federationmetadata/2007-06/federationmetadata.xml";
160+
options.BackchannelHttpHandler = new WaadMetadataDocumentHandler();
161+
options.StateDataFormat = new CustomStateDataFormat();
162+
options.SecurityTokenHandlers = new List<ISecurityTokenValidator>() { new TestSecurityTokenValidator() };
163+
options.UseTokenLifetime = false;
164+
options.AllowUnsolicitedLogins = allowUnsolicited;
165+
options.Events = new WsFederationEvents()
166166
{
167-
// Sign in message
168-
context.ProtocolMessage.Wctx = "customValue";
169-
}
167+
MessageReceived = context =>
168+
{
169+
if (!context.ProtocolMessage.Parameters.TryGetValue("suppressWctx", out var suppress))
170+
{
171+
Assert.True(context.ProtocolMessage.Wctx.Equals("customValue"), "wctx is not my custom value");
172+
}
173+
context.HttpContext.Items["MessageReceived"] = true;
174+
return Task.FromResult(0);
175+
},
176+
RedirectToIdentityProvider = context =>
177+
{
178+
if (context.ProtocolMessage.IsSignInMessage)
179+
{
180+
// Sign in message
181+
context.ProtocolMessage.Wctx = "customValue";
182+
}
170183

171-
return Task.FromResult(0);
172-
},
173-
SecurityTokenReceived = context =>
174-
{
175-
context.HttpContext.Items["SecurityTokenReceived"] = true;
176-
return Task.FromResult(0);
177-
},
178-
SecurityTokenValidated = context =>
179-
{
180-
Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked");
181-
Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked");
184+
return Task.FromResult(0);
185+
},
186+
SecurityTokenReceived = context =>
187+
{
188+
context.HttpContext.Items["SecurityTokenReceived"] = true;
189+
return Task.FromResult(0);
190+
},
191+
SecurityTokenValidated = context =>
192+
{
193+
Assert.True((bool)context.HttpContext.Items["MessageReceived"], "MessageReceived notification not invoked");
194+
Assert.True((bool)context.HttpContext.Items["SecurityTokenReceived"], "SecurityTokenReceived notification not invoked");
182195

183-
if (context.Principal != null)
184-
{
185-
var identity = context.Principal.Identities.Single();
186-
identity.AddClaim(new Claim("ReturnEndpoint", "true"));
187-
identity.AddClaim(new Claim("Authenticated", "true"));
188-
identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String));
189-
}
190-
191-
return Task.FromResult(0);
192-
},
193-
AuthenticationFailed = context =>
194-
{
195-
context.HttpContext.Items["AuthenticationFailed"] = true;
196-
//Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked.
197-
context.HttpContext.Request.Path = new PathString("/AuthenticationFailed");
198-
context.SkipHandler();
199-
return Task.FromResult(0);
200-
}
201-
};
202-
})
203-
.AddCookie();
196+
if (context.Principal != null)
197+
{
198+
var identity = context.Principal.Identities.Single();
199+
identity.AddClaim(new Claim("ReturnEndpoint", "true"));
200+
identity.AddClaim(new Claim("Authenticated", "true"));
201+
identity.AddClaim(new Claim(identity.RoleClaimType, "Guest", ClaimValueTypes.String));
202+
}
203+
204+
return Task.FromResult(0);
205+
},
206+
AuthenticationFailed = context =>
207+
{
208+
context.HttpContext.Items["AuthenticationFailed"] = true;
209+
//Change the request url to something different and skip Wsfed. This new url will handle the request and let us know if this notification was invoked.
210+
context.HttpContext.Request.Path = new PathString("/AuthenticationFailed");
211+
context.SkipHandler();
212+
return Task.FromResult(0);
213+
}
214+
};
215+
});
216+
});
217+
var server = new TestServer(builder);
218+
return server.CreateClient();
204219
}
205220

206221
private void ConfigureApp(IApplicationBuilder app)

0 commit comments

Comments
 (0)