Skip to content

Commit 5b8d93c

Browse files
KahbaziTratcher
authored andcommitted
Check iss in odic sign-out (#6378)
1 parent 3f1760c commit 5b8d93c

File tree

5 files changed

+120
-9
lines changed

5 files changed

+120
-9
lines changed

src/Security/Authentication/OpenIdConnect/src/LoggingExtensions.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ internal static class LoggingExtensions
5757
private static Action<ILogger, Exception> _remoteSignOutSessionIdInvalid;
5858
private static Action<ILogger, string, Exception> _authenticationSchemeSignedOut;
5959
private static Action<ILogger, string, string, Exception> _handleChallenge;
60+
private static Action<ILogger, Exception> _remoteSignOutIssuerMissing;
61+
private static Action<ILogger, Exception> _remoteSignOutIssuerInvalid;
6062

6163
static LoggingExtensions()
6264
{
@@ -264,7 +266,17 @@ static LoggingExtensions()
264266
_handleChallenge = LoggerMessage.Define<string, string>(
265267
eventId: new EventId(53, "HandleChallenge"),
266268
logLevel: LogLevel.Debug,
267-
formatString: "HandleChallenge with Location: {Location}; and Set-Cookie: {Cookie}.");
269+
formatString: "HandleChallenge with Location: {Location}; and Set-Cookie: {Cookie}.");
270+
_remoteSignOutIssuerMissing = LoggerMessage.Define(
271+
eventId: new EventId(54, "RemoteSignOutIssuerMissing"),
272+
logLevel: LogLevel.Error,
273+
formatString: "The remote signout request was ignored because the 'iss' parameter " +
274+
"was missing, which may indicate an unsolicited logout.");
275+
_remoteSignOutIssuerInvalid = LoggerMessage.Define(
276+
eventId: new EventId(55, "RemoteSignOutIssuerInvalid"),
277+
logLevel: LogLevel.Error,
278+
formatString: "The remote signout request was ignored because the 'iss' parameter didn't match " +
279+
"the expected value, which may indicate an unsolicited logout.");
268280
}
269281

270282
public static void UpdatingConfiguration(this ILogger logger)
@@ -514,5 +526,15 @@ public static void AuthenticationSchemeSignedOut(this ILogger logger, string aut
514526

515527
public static void HandleChallenge(this ILogger logger, string location, string cookie)
516528
=> _handleChallenge(logger, location, cookie, null);
529+
530+
public static void RemoteSignOutIssuerMissing(this ILogger logger)
531+
{
532+
_remoteSignOutIssuerMissing(logger, null);
533+
}
534+
535+
public static void RemoteSignOutIssuerInvalid(this ILogger logger)
536+
{
537+
_remoteSignOutIssuerInvalid(logger, null);
538+
}
517539
}
518540
}

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,9 @@ protected virtual async Task<bool> HandleRemoteSignOutAsync()
123123
// If the identifier cannot be found, bypass the session identifier checks: this may indicate that the
124124
// authentication cookie was already cleared, that the session identifier was lost because of a lossy
125125
// external/application cookie conversion or that the identity provider doesn't support sessions.
126-
var sid = (await Context.AuthenticateAsync(Options.SignOutScheme))
127-
?.Principal
128-
?.FindFirst(JwtRegisteredClaimNames.Sid)
129-
?.Value;
126+
var principal = (await Context.AuthenticateAsync(Options.SignOutScheme))?.Principal;
127+
128+
var sid = principal?.FindFirst(JwtRegisteredClaimNames.Sid)?.Value;
130129
if (!string.IsNullOrEmpty(sid))
131130
{
132131
// Ensure a 'sid' parameter was sent by the identity provider.
@@ -143,6 +142,23 @@ protected virtual async Task<bool> HandleRemoteSignOutAsync()
143142
}
144143
}
145144

145+
var iss = principal?.FindFirst(JwtRegisteredClaimNames.Iss)?.Value;
146+
if (!string.IsNullOrEmpty(iss))
147+
{
148+
// Ensure a 'iss' parameter was sent by the identity provider.
149+
if (string.IsNullOrEmpty(message.Iss))
150+
{
151+
Logger.RemoteSignOutIssuerMissing();
152+
return true;
153+
}
154+
// Ensure the 'iss' parameter corresponds to the 'iss' stored in the authentication ticket.
155+
if (!string.Equals(iss, message.Iss, StringComparison.Ordinal))
156+
{
157+
Logger.RemoteSignOutIssuerInvalid();
158+
return true;
159+
}
160+
}
161+
146162
Logger.RemoteSignOut();
147163

148164
// We've received a remote sign-out request

src/Security/Authentication/test/OpenIdConnect/OpenIdConnectTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Globalization;
66
using System.Linq;
77
using System.Net;
8+
using System.Security.Claims;
89
using System.Text.Encodings.Web;
910
using System.Threading.Tasks;
1011
using Microsoft.AspNetCore.Authentication.Cookies;
@@ -294,6 +295,77 @@ public async Task SignOut_WithMissingConfig_Throws()
294295
Assert.Equal("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.", exception.Message);
295296
}
296297

298+
[Fact]
299+
public async Task RemoteSignOut_WithMissingIssuer()
300+
{
301+
var settings = new TestSettings(o =>
302+
{
303+
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
304+
o.Authority = TestServerBuilder.DefaultAuthority;
305+
o.ClientId = "Test Id";
306+
});
307+
var server = settings.CreateTestServer(handler: async context =>
308+
{
309+
var claimsIdentity = new ClaimsIdentity("Cookies");
310+
claimsIdentity.AddClaim(new Claim("iss", "test"));
311+
await context.SignInAsync(new ClaimsPrincipal(claimsIdentity));
312+
});
313+
314+
var signInTransaction = await server.SendAsync(DefaultHost);
315+
316+
var remoteSignOutTransaction = await server.SendAsync(DefaultHost + "/signout-oidc", signInTransaction.AuthenticationCookieValue);
317+
Assert.Equal(HttpStatusCode.OK, remoteSignOutTransaction.Response.StatusCode);
318+
Assert.DoesNotContain(remoteSignOutTransaction.Response.Headers, h => h.Key == "Set-Cookie");
319+
320+
}
321+
322+
[Fact]
323+
public async Task RemoteSignOut_WithInvalidIssuer()
324+
{
325+
var settings = new TestSettings(o =>
326+
{
327+
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
328+
o.Authority = TestServerBuilder.DefaultAuthority;
329+
o.ClientId = "Test Id";
330+
});
331+
var server = settings.CreateTestServer(handler: async context =>
332+
{
333+
var claimsIdentity = new ClaimsIdentity("Cookies");
334+
claimsIdentity.AddClaim(new Claim("iss", "test"));
335+
await context.SignInAsync(new ClaimsPrincipal(claimsIdentity));
336+
});
337+
338+
var signInTransaction = await server.SendAsync(DefaultHost);
339+
340+
var remoteSignOutTransaction = await server.SendAsync(DefaultHost + "/signout-oidc?iss=invalid", signInTransaction.AuthenticationCookieValue);
341+
Assert.Equal(HttpStatusCode.OK, remoteSignOutTransaction.Response.StatusCode);
342+
Assert.DoesNotContain(remoteSignOutTransaction.Response.Headers, h => h.Key == "Set-Cookie");
343+
}
344+
345+
[Fact]
346+
public async Task RemoteSignOut_Get_Successful()
347+
{
348+
var settings = new TestSettings(o =>
349+
{
350+
o.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
351+
o.Authority = TestServerBuilder.DefaultAuthority;
352+
o.ClientId = "Test Id";
353+
});
354+
var server = settings.CreateTestServer(handler: async context =>
355+
{
356+
var claimsIdentity = new ClaimsIdentity("Cookies");
357+
claimsIdentity.AddClaim(new Claim("iss", "test"));
358+
claimsIdentity.AddClaim(new Claim("sid", "something"));
359+
await context.SignInAsync(new ClaimsPrincipal(claimsIdentity));
360+
});
361+
362+
var signInTransaction = await server.SendAsync(DefaultHost);
363+
364+
var remoteSignOutTransaction = await server.SendAsync(DefaultHost + "/signout-oidc?iss=test&sid=something", signInTransaction.AuthenticationCookieValue);
365+
Assert.Equal(HttpStatusCode.OK, remoteSignOutTransaction.Response.StatusCode);
366+
Assert.Contains(remoteSignOutTransaction.Response.Headers, h => h.Key == "Set-Cookie");
367+
}
368+
297369
// Test Cases for calculating the expiration time of cookie from cookie name
298370
[Fact]
299371
public void NonceCookieExpirationTime()

src/Security/Authentication/test/OpenIdConnect/TestSettings.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Threading.Tasks;
1515
using System.Xml.Linq;
1616
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
17+
using Microsoft.AspNetCore.Http;
1718
using Microsoft.AspNetCore.TestHost;
1819
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
1920
using Xunit;
@@ -46,7 +47,7 @@ public TestSettings(Action<OpenIdConnectOptions> configure)
4647

4748
public string ExpectedState { get; set; }
4849

49-
public TestServer CreateTestServer(AuthenticationProperties properties = null) => TestServerBuilder.CreateServer(_configureOptions, handler: null, properties: properties);
50+
public TestServer CreateTestServer(AuthenticationProperties properties = null, Func<HttpContext, Task> handler = null) => TestServerBuilder.CreateServer(_configureOptions, handler: handler, properties: properties);
5051

5152
public IDictionary<string, string> ValidateChallengeFormPost(string responseBody, params string[] parametersToValidate)
5253
{
@@ -347,4 +348,4 @@ private async Task<HttpResponseMessage> ReturnResource(string resource)
347348
}
348349
}
349350
}
350-
}
351+
}

src/Security/Authentication/test/OpenIdConnect/TestTransaction.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public string AuthenticationCookieValue
2626
{
2727
if (SetCookie != null && SetCookie.Count > 0)
2828
{
29-
var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie="));
29+
var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookies="));
3030
if (authCookie != null)
3131
{
3232
return authCookie.Substring(0, authCookie.IndexOf(';'));
@@ -37,4 +37,4 @@ public string AuthenticationCookieValue
3737
}
3838
}
3939
}
40-
}
40+
}

0 commit comments

Comments
 (0)