Browse Source

Implement RP-initiated logout in the client stack

pull/1506/head
Kévin Chalet 4 years ago
parent
commit
a99a6cdace
  1. 102
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  2. 27
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  3. 8
      sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml
  4. 30
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs
  5. 14
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  6. 76
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  7. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs
  8. 31
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  9. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml
  10. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs
  11. 4
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  12. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  13. 2
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  14. 15
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  15. 5
      src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
  16. 10
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs
  17. 6
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
  18. 1
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs
  19. 47
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  20. 46
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs
  21. 105
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Session.cs
  22. 260
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  23. 8
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs
  24. 10
      src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs
  25. 6
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  26. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs
  27. 44
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  28. 24
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs
  29. 93
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Session.cs
  30. 283
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  31. 8
      src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs
  32. 46
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  33. 7
      src/OpenIddict.Client/OpenIddictClientEndpointType.cs
  34. 190
      src/OpenIddict.Client/OpenIddictClientEvents.Session.cs
  35. 111
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  36. 4
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  37. 54
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  38. 47
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  39. 450
      src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs
  40. 574
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  41. 5
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  42. 5
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  43. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  44. 1
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

102
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs

@ -7,10 +7,8 @@ using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{
@ -55,7 +53,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{
// Note: the OWIN host requires appending the #string suffix to indicate
// that the "identity_provider" property is a public string parameter.
properties.Dictionary[Parameters.IdentityProvider + PropertyTypes.String] = "github";
properties.Dictionary[Parameters.IdentityProvider + OpenIddictClientOwinConstants.PropertyTypes.String] = "github";
}
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
@ -63,11 +61,58 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
return new EmptyResult();
}
[HttpPost, Route("~/logout"), ValidateAntiForgeryToken]
public async Task<ActionResult> LogOut(string returnUrl)
{
var context = HttpContext.GetOwinContext();
// Retrieve the identity stored in the local authentication cookie. If it's not available,
// this indicate that the user is already logged out locally (or has not logged in yet).
var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType);
if (result is not { Identity: ClaimsIdentity identity })
{
// Only allow local return URLs to prevent open redirect attacks.
return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
}
// Remove the local authentication cookie before triggering a redirection to the remote server.
context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
// Resolve the issuer of the user identifier claim stored in the local authentication cookie.
// If the issuer is known to support remote sign-out, ask OpenIddict to initiate a logout request.
var issuer = identity.Claims.Select(claim => claim.Issuer).First();
if (issuer is "https://localhost:44349/")
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientOwinConstants.Properties.Issuer] = issuer,
// While not required, the specification encourages sending an id_token_hint
// parameter containing an identity token returned by the server for this user.
[OpenIddictClientOwinConstants.Properties.IdentityTokenHint] =
result.Properties.Dictionary[OpenIddictClientOwinConstants.Tokens.BackchannelIdentityToken]
})
{
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
context.Authentication.SignOut(properties, OpenIddictClientOwinDefaults.AuthenticationType);
return new EmptyResult();
}
// Only allow local return URLs to prevent open redirect attacks.
return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
}
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[AcceptVerbs("GET", "POST"), Route("~/signin-{provider}")]
public async Task<ActionResult> Callback()
[AcceptVerbs("GET", "POST"), Route("~/callback/login/{provider}")]
public async Task<ActionResult> LogInCallback()
{
var context = HttpContext.GetOwinContext();
@ -123,12 +168,22 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// The antiforgery components require an "identityprovider" claim, which
// is mapped from the authorization server claim returned by OpenIddict.
{ Type: Claims.AuthorizationServer }
=> new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the nameidentifier and name claims.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Preserve the basic claims that are necessary for the application to work correctly.
{
Type: ClaimTypes.NameIdentifier or
ClaimTypes.Name or
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"
} => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
@ -137,11 +192,6 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
_ => false
}));
// The antiforgery components require both the ClaimTypes.NameIdentifier and identityprovider claims
// so the latter is manually added using the issuer identity resolved from the remote server.
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
result.Identity.GetClaim(Claims.AuthorizationServer)));
var identity = new ClaimsIdentity(claims,
authenticationType: CookieAuthenticationDefaults.AuthenticationType,
nameType: ClaimTypes.Name,
@ -155,7 +205,11 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{ Key: ".redirect" } => true,
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
{ Key: Tokens.BackchannelAccessToken or Tokens.RefreshToken } => true,
{
Key: OpenIddictClientOwinConstants.Tokens.BackchannelAccessToken or
OpenIddictClientOwinConstants.Tokens.BackchannelIdentityToken or
OpenIddictClientOwinConstants.Tokens.RefreshToken
} => true,
// Don't add the other properties to the external cookie.
_ => false
@ -166,20 +220,22 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
return Redirect(properties.RedirectUri);
}
[AcceptVerbs("GET", "POST"), Route("~/logout")]
public ActionResult LogOut()
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[AcceptVerbs("GET", "POST"), Route("~/callback/logout/{provider}")]
public async Task<ActionResult> LogOutCallback()
{
var context = HttpContext.GetOwinContext();
// Ask the cookies middleware to delete the local cookie created when the user agent
// is redirected from the identity provider after a successful authorization flow.
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
// Retrieve the data stored by OpenIddict in the state token created when the logout was triggered.
var result = await context.Authentication.AuthenticateAsync(OpenIddictClientOwinDefaults.AuthenticationType);
context.Authentication.SignOut(properties, CookieAuthenticationDefaults.AuthenticationType);
return Redirect(properties.RedirectUri);
// In this sample, the local authentication cookie is always removed before the user agent is redirected
// to the authorization server. Applications that prefer delaying the removal of the local cookie can
// remove the corresponding code from the logout action and remove the authentication cookie in this action.
return Redirect(result.Properties.RedirectUri);
}
}
}

27
sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs

@ -73,10 +73,14 @@ namespace OpenIddict.Sandbox.AspNet.Client
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris(
"/signin-local",
"/signin-github",
"/signin-google",
"/signin-twitter");
"/callback/login/local",
"/callback/login/github",
"/callback/login/google",
"/callback/login/twitter");
// Enable the post-logout redirection endpoints needed to handle the callback stage.
options.SetPostLogoutRedirectionEndpointUris(
"/callback/logout/local");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -85,7 +89,8 @@ namespace OpenIddict.Sandbox.AspNet.Client
// Register the OWIN host and configure the OWIN-specific options.
options.UseOwin()
.EnableRedirectionEndpointPassthrough();
.EnableRedirectionEndpointPassthrough()
.EnablePostLogoutRedirectionEndpointPassthrough();
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
@ -97,8 +102,10 @@ namespace OpenIddict.Sandbox.AspNet.Client
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
RedirectUri = new Uri("https://localhost:44378/signin-local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("https://localhost:44378/callback/login/local", UriKind.Absolute),
PostLogoutRedirectUri = new Uri("https://localhost:44378/callback/logout/local", UriKind.Absolute)
});
// Register the Web providers integrations.
@ -107,20 +114,20 @@ namespace OpenIddict.Sandbox.AspNet.Client
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44378/signin-github", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44378/callback/login/github", UriKind.Absolute)
})
.AddGoogle(new()
{
ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com",
ClientSecret = "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",
RedirectUri = new Uri("https://localhost:44378/signin-google", UriKind.Absolute),
RedirectUri = new Uri("https://localhost:44378/callback/login/google", UriKind.Absolute),
Scopes = { Scopes.Profile }
})
.AddTwitter(new()
{
ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ",
ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS",
RedirectUri = new Uri("https://localhost:44378/signin-twitter", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44378/callback/login/twitter", UriKind.Absolute)
});
});

8
sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml

@ -21,14 +21,18 @@
if (User is ClaimsPrincipal principal &&
principal.FindFirst(ClaimTypes.NameIdentifier)?.Issuer is "https://localhost:44349/")
{
<form action="~/" method="post">
<form action="/" method="post">
@Html.AntiForgeryToken()
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
}
<a class="btn btn-lg btn-danger" href="/logout">Sign out</a>
<form action="/logout" method="post">
@Html.AntiForgeryToken()
<button class="btn btn-lg btn-danger" type="submit">Sign out</button>
</form>
}
else

30
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs

@ -7,10 +7,8 @@ using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions;
using OpenIddict.Client.Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
namespace OpenIddict.Sandbox.AspNet.Server.Controllers
{
@ -19,8 +17,8 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[AcceptVerbs("GET", "POST"), Route("~/signin-{provider}")]
public async Task<ActionResult> Callback()
[AcceptVerbs("GET", "POST"), Route("~/callback/login/{provider}")]
public async Task<ActionResult> LogInCallback()
{
var context = HttpContext.GetOwinContext();
@ -76,12 +74,22 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
{ Type: Claims.Name }
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// The antiforgery components require an "identityprovider" claim, which
// is mapped from the authorization server claim returned by OpenIddict.
{ Type: Claims.AuthorizationServer }
=> new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
claim.Value, claim.ValueType, claim.Issuer),
_ => claim
})
.Where(claim => claim switch
{
// Preserve the nameidentifier and name claims.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Preserve the basic claims that are necessary for the application to work correctly.
{
Type: ClaimTypes.NameIdentifier or
ClaimTypes.Name or
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"
} => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,
@ -90,11 +98,6 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
_ => false
}));
// The antiforgery components require both the ClaimTypes.NameIdentifier and identityprovider claims
// so the latter is manually added using the issuer identity resolved from the remote server.
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider",
result.Identity.GetClaim(Claims.AuthorizationServer)));
// Note: when using external authentication providers with ASP.NET Identity,
// the user identity MUST be added to the external authentication cookie scheme.
var identity = new ClaimsIdentity(claims,
@ -110,7 +113,10 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
{ Key: ".redirect" } => true,
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
{ Key: Tokens.BackchannelAccessToken or Tokens.RefreshToken } => true,
{
Key: OpenIddictClientOwinConstants.Tokens.BackchannelAccessToken or
OpenIddictClientOwinConstants.Tokens.RefreshToken
} => true,
// Don't add the other properties to the external cookie.
_ => false

14
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -72,7 +72,11 @@ namespace OpenIddict.Sandbox.AspNet.Server
DisplayName = "MVC client application",
RedirectUris =
{
new Uri("https://localhost:44378/signin-local")
new Uri("https://localhost:44378/callback/login/local")
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44378/callback/logout/local")
},
Permissions =
{
@ -87,10 +91,6 @@ namespace OpenIddict.Sandbox.AspNet.Server
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44378/Account/SignOutCallback")
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
@ -129,7 +129,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/signin-github");
options.SetRedirectionEndpointUris("/callback/login/github");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -149,7 +149,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44349/signin-github", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44349/callback/login/github", UriKind.Absolute)
});
})

76
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs

@ -52,11 +52,55 @@ public class AuthenticationController : Controller
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
[HttpPost("~/logout"), ValidateAntiForgeryToken]
public async Task<ActionResult> LogOut(string returnUrl)
{
// Retrieve the identity stored in the local authentication cookie. If it's not available,
// this indicate that the user is already logged out locally (or has not logged in yet).
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (result is not { Principal.Identity: ClaimsIdentity identity })
{
// Only allow local return URLs to prevent open redirect attacks.
return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
}
// Remove the local authentication cookie before triggering a redirection to the remote server.
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// Resolve the issuer of the user identifier claim stored in the local authentication cookie.
// If the issuer is known to support remote sign-out, ask OpenIddict to initiate a logout request.
var issuer = identity.Claims.Select(claim => claim.Issuer).First();
if (issuer is "https://localhost:44395/")
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer,
// While not required, the specification encourages sending an id_token_hint
// parameter containing an identity token returned by the server for this user.
[OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] =
result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
})
{
// Only allow local return URLs to prevent open redirect attacks.
RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
};
// Ask the OpenIddict client middleware to redirect the user agent to the identity provider.
return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
// Only allow local return URLs to prevent open redirect attacks.
return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
}
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/signin-{provider}"), HttpPost("~/signin-{provider}")]
public async Task<ActionResult> Callback()
[HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
public async Task<ActionResult> LogInCallback()
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
@ -114,7 +158,7 @@ public class AuthenticationController : Controller
})
.Where(claim => claim switch
{
// Preserve the nameidentifier and name claims.
// Preserve the basic claims that are necessary for the application to work correctly.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
@ -136,9 +180,10 @@ public class AuthenticationController : Controller
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
{
// Preserve the access and refresh tokens returned in the token response, if available.
// Preserve the access, identity and refresh tokens returned in the token response, if available.
{
Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
} => true,
@ -154,16 +199,19 @@ public class AuthenticationController : Controller
return Redirect(properties.RedirectUri);
}
[HttpGet("~/logout"), HttpPost("~/logout")]
public ActionResult LogOut()
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/callback/logout/{provider}"), HttpPost("~/callback/logout/{provider}"), IgnoreAntiforgeryToken]
public async Task<ActionResult> LogOutCallback()
{
// Ask the cookies middleware to delete the local cookie created when the user agent
// is redirected from the identity provider after a successful authorization flow.
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
// Retrieve the data stored by OpenIddict in the state token created when the logout was triggered.
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
// In this sample, the local authentication cookie is always removed before the user agent is redirected
// to the authorization server. Applications that prefer delaying the removal of the local cookie can
// remove the corresponding code from the logout action and remove the authentication cookie in this action.
return SignOut(properties, CookieAuthenticationDefaults.AuthenticationScheme);
return Redirect(result!.Properties!.RedirectUri);
}
}

2
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs

@ -17,7 +17,7 @@ public class HomeController : Controller
[HttpGet("~/")]
public ActionResult Index() => View();
[Authorize, HttpPost("~/")]
[Authorize, HttpPost("~/"), ValidateAntiForgeryToken]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
{
var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme,

31
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -81,11 +81,15 @@ public class Startup
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris(
"/signin-local",
"/signin-github",
"/signin-google",
"/signin-reddit",
"/signin-twitter");
"/callback/login/local",
"/callback/login/github",
"/callback/login/google",
"/callback/login/reddit",
"/callback/login/twitter");
// Enable the post-logout redirection endpoints needed to handle the callback stage.
options.SetPostLogoutRedirectionEndpointUris(
"/callback/logout/local");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -95,7 +99,8 @@ public class Startup
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableRedirectionEndpointPassthrough();
.EnableRedirectionEndpointPassthrough()
.EnablePostLogoutRedirectionEndpointPassthrough();
// Register the System.Net.Http integration.
options.UseSystemNetHttp();
@ -107,8 +112,10 @@ public class Startup
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
RedirectUri = new Uri("https://localhost:44381/signin-local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("https://localhost:44381/callback/login/local", UriKind.Absolute),
PostLogoutRedirectUri = new Uri("https://localhost:44381/callback/logout/local", UriKind.Absolute),
});
// Register the Web providers integrations.
@ -117,20 +124,20 @@ public class Startup
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44381/signin-github", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44381/callback/login/github", UriKind.Absolute)
})
.AddGoogle(new()
{
ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com",
ClientSecret = "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",
RedirectUri = new Uri("https://localhost:44381/signin-google", UriKind.Absolute),
RedirectUri = new Uri("https://localhost:44381/callback/login/google", UriKind.Absolute),
Scopes = { Scopes.Profile }
})
.AddReddit(new()
{
ClientId = "vDLNqhrkwrvqHgnoBWF3og",
ClientSecret = "Tpab28Dz0upyZLqn7AN3GFD1O-zaAw",
RedirectUri = new Uri("https://localhost:44381/signin-reddit", UriKind.Absolute),
RedirectUri = new Uri("https://localhost:44381/callback/login/reddit", UriKind.Absolute),
ProductName = "DemoApp",
ProductVersion = "1.0.0"
})
@ -138,7 +145,7 @@ public class Startup
{
ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ",
ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS",
RedirectUri = new Uri("https://localhost:44381/signin-twitter", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44381/callback/login/twitter", UriKind.Absolute)
});
});

6
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml

@ -20,12 +20,14 @@
if (User.FindFirst(ClaimTypes.NameIdentifier)?.Issuer is "https://localhost:44395/")
{
<form action="/" method="post">
<form asp-action="Index" asp-controller="Home" method="post">
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
}
<a class="btn btn-lg btn-danger" href="/logout">Sign out</a>
<form asp-action="Logout" asp-controller="Authentication" method="post">
<button class="btn btn-lg btn-danger" type="submit">Sign out</button>
</form>
}
else

6
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs

@ -12,8 +12,8 @@ public class AuthenticationController : Controller
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/signin-{provider}"), HttpPost("~/signin-{provider}")]
public async Task<ActionResult> Callback()
[HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
public async Task<ActionResult> LogInCallback()
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
@ -71,7 +71,7 @@ public class AuthenticationController : Controller
})
.Where(claim => claim switch
{
// Preserve the nameidentifier and name claims.
// Preserve the basic claims that are necessary for the application to work correctly.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.

4
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -74,7 +74,7 @@ public class Startup
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/signin-github");
options.SetRedirectionEndpointUris("/callback/login/github");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -95,7 +95,7 @@ public class Startup
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44395/signin-github", UriKind.Absolute)
RedirectUri = new Uri("https://localhost:44395/callback/login/github", UriKind.Absolute)
});
})

6
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -40,7 +40,11 @@ public class Worker : IHostedService
},
RedirectUris =
{
new Uri("https://localhost:44381/signin-local")
new Uri("https://localhost:44381/callback/login/local")
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44381/callback/logout/local")
},
Permissions =
{

2
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -125,12 +125,14 @@ public static class OpenIddictConstants
public const string CreationDate = "oi_crt_dt";
public const string DeviceCodeId = "oi_dvc_id";
public const string DeviceCodeLifetime = "oi_dvc_lft";
public const string EndpointType = "oi_ept_typ";
public const string ExpirationDate = "oi_exp_dt";
public const string GrantType = "oi_grt_typ";
public const string HostProperties = "oi_hst_props";
public const string IdentityTokenLifetime = "oi_idt_lft";
public const string Issuer = "oi_iss";
public const string Nonce = "oi_nce";
public const string PostLogoutRedirectUri = "oi_pstlgt_reduri";
public const string Presenter = "oi_prst";
public const string RedirectUri = "oi_reduri";
public const string RefreshTokenLifetime = "oi_reft_lft";

15
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1316,6 +1316,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0339" xml:space="preserve">
<value>The request forgery protection claim cannot be resolved from the state token.</value>
</data>
<data name="ID0340" xml:space="preserve">
<value>The endpoint type associated with the state token cannot be resolved.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -1739,6 +1742,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID2141" xml:space="preserve">
<value>The client application is not allowed to use the specified identity token hint.</value>
</data>
<data name="ID2142" xml:space="preserve">
<value>The specified state token is not suitable for the requested operation.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -2344,6 +2350,15 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6198" xml:space="preserve">
<value>The logout request was rejected because the identity token used as a hint was issued to a different client.</value>
</data>
<data name="ID6199" xml:space="preserve">
<value>The post-logout redirection request was successfully extracted: {Request}.</value>
</data>
<data name="ID6200" xml:space="preserve">
<value>The post-logout redirection request was successfully validated.</value>
</data>
<data name="ID6201" xml:space="preserve">
<value>The post-logout redirection request was successfully validated.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

5
src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs

@ -31,6 +31,11 @@ public class OpenIddictConfiguration
/// </summary>
public HashSet<string> CodeChallengeMethodsSupported { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the address of the end session endpoint.
/// </summary>
public Uri? EndSessionEndpoint { get; set; }
/// <summary>
/// Gets the grant types supported by the server.
/// </summary>

10
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs

@ -47,6 +47,16 @@ public class OpenIddictClientAspNetCoreBuilder
return this;
}
/// <summary>
/// Enables the pass-through mode for the OpenID Connect post-logout redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
/// <returns>The <see cref="OpenIddictClientAspNetCoreBuilder"/>.</returns>
public OpenIddictClientAspNetCoreBuilder EnablePostLogoutRedirectionEndpointPassthrough()
=> Configure(options => options.EnablePostLogoutRedirectionEndpointPassthrough = true);
/// <summary>
/// Enables the pass-through mode for the OpenID Connect redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.

6
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs

@ -15,9 +15,11 @@ public static class OpenIddictClientAspNetCoreConstants
{
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_id_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_identity_token_principal";
public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_id_token_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal";
public const string IdentityTokenHint = ".identity_token_hint";
public const string LoginHint = ".login_hint";
public const string Issuer = ".issuer";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";

1
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs

@ -40,6 +40,7 @@ public static class OpenIddictClientAspNetCoreExtensions
// Register the built-in filters used by the default OpenIddict ASP.NET Core client event handlers.
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireHttpRequest>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectionEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireRedirectionEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireStatusCodePagesIntegrationEnabled>();

47
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -19,7 +19,8 @@ namespace OpenIddict.Client.AspNetCore;
/// Provides the logic necessary to extract, validate and handle OpenID Connect requests.
/// </summary>
public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddictClientAspNetCoreOptions>,
IAuthenticationRequestHandler
IAuthenticationRequestHandler,
IAuthenticationSignOutHandler
{
private readonly IOpenIddictClientDispatcher _dispatcher;
private readonly IOpenIddictClientFactory _factory;
@ -150,6 +151,8 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal),
OpenIddictClientEndpointType.PostLogoutRedirection => context.StateTokenPrincipal,
_ => null
};
@ -374,4 +377,46 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
/// <inheritdoc/>
protected override Task HandleForbiddenAsync(AuthenticationProperties? properties)
=> HandleChallengeAsync(properties);
/// <inheritdoc/>
public async Task SignOutAsync(AuthenticationProperties? properties)
{
var transaction = Context.Features.Get<OpenIddictClientAspNetCoreFeature>()?.Transaction ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0112));
var context = new ProcessSignOutContext(transaction)
{
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
transaction.Properties[typeof(AuthenticationProperties).FullName!] = properties ?? new AuthenticationProperties();
await _dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0111));
}
}
}

46
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs

@ -17,14 +17,13 @@ namespace OpenIddict.Client.AspNetCore;
public static class OpenIddictClientAspNetCoreHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the authorization endpoint.
/// Represents a filter that excludes the associated handlers if error pass-through was not enabled.
/// </summary>
public class RequireRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
public class RequireErrorPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public RequireRedirectionEndpointPassthroughEnabled(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
public RequireErrorPassthroughEnabled(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public ValueTask<bool> IsActiveAsync(BaseContext context)
@ -34,18 +33,35 @@ public static class OpenIddictClientAspNetCoreHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(_options.CurrentValue.EnableRedirectionEndpointPassthrough);
return new(_options.CurrentValue.EnableErrorPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if error pass-through was not enabled.
/// Represents a filter that excludes the associated handlers if no ASP.NET Core request can be found.
/// </summary>
public class RequireErrorPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
public class RequireHttpRequest : IOpenIddictClientHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetHttpRequest() is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the post-logout redirection endpoint.
/// </summary>
public class RequirePostLogoutRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public RequireErrorPassthroughEnabled(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
public RequirePostLogoutRedirectionEndpointPassthroughEnabled(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public ValueTask<bool> IsActiveAsync(BaseContext context)
@ -55,15 +71,21 @@ public static class OpenIddictClientAspNetCoreHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(_options.CurrentValue.EnableErrorPassthrough);
return new(_options.CurrentValue.EnablePostLogoutRedirectionEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no ASP.NET Core request can be found.
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the redirection endpoint.
/// </summary>
public class RequireHttpRequest : IOpenIddictClientHandlerFilter<BaseContext>
public class RequireRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public RequireRedirectionEndpointPassthroughEnabled(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
@ -71,7 +93,7 @@ public static class OpenIddictClientAspNetCoreHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetHttpRequest() is not null);
return new(_options.CurrentValue.EnableRedirectionEndpointPassthrough);
}
}

105
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.Session.cs

@ -0,0 +1,105 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.WebUtilities;
namespace OpenIddict.Client.AspNetCore;
public static partial class OpenIddictClientAspNetCoreHandlers
{
public static class Session
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Session request processing:
*/
ProcessQueryRequest.Descriptor,
/*
* Post-logout redirection request extraction:
*/
ExtractGetOrPostRequest<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
/*
* Post-logout redirection request handling:
*/
EnablePassthroughMode<HandlePostLogoutRedirectionRequestContext, RequirePostLogoutRedirectionEndpointPassthroughEnabled>.Descriptor,
/*
* Post-logout redirection response handling:
*/
AttachHttpResponseCode<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyPostLogoutRedirectionResponseContext, RequirePostLogoutRedirectionEndpointPassthroughEnabled>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessLocalErrorResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible for processing authorization requests using 302 redirects.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ProcessQueryRequest : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyLogoutRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessQueryRequest>()
.SetOrder(50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyLogoutRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
#if SUPPORTS_MULTIPLE_VALUES_IN_QUERYHELPERS
var location = QueryHelpers.AddQueryString(context.EndSessionEndpoint,
from parameter in context.Request.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select KeyValuePair.Create(parameter.Key, value));
#else
var location = context.EndSessionEndpoint;
foreach (var (key, value) in
from parameter in context.Request.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = QueryHelpers.AddQueryString(location, key, value);
}
#endif
response.Redirect(location);
context.HandleRequest();
return default;
}
}
}
}

260
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -37,14 +37,21 @@ public static partial class OpenIddictClientAspNetCoreHandlers
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ValidateRedirectUri.Descriptor,
ValidateEndpointUri.Descriptor,
/*
* Challenge processing:
*/
GenerateCorrelationCookie.Descriptor,
ResolveHostChallengeParameters.Descriptor)
.AddRange(Authentication.DefaultHandlers);
ResolveHostChallengeParameters.Descriptor,
GenerateLoginCorrelationCookie.Descriptor,
/*
* Sign-out processing:
*/
ResolveHostSignOutParameters.Descriptor,
GenerateLogoutCorrelationCookie.Descriptor)
.AddRange(Authentication.DefaultHandlers)
.AddRange(Session.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for inferring the endpoint type from the request address.
@ -76,8 +83,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
context.EndpointType =
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection :
OpenIddictClientEndpointType.Unknown;
Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection :
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection :
OpenIddictClientEndpointType.Unknown;
return default;
@ -215,8 +223,8 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
/// <summary>
/// Contains the logic responsible for validating the correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.
/// Contains the logic responsible for validating the correlation cookie that serves as a protection
/// against state token injection, forged requests, denial of service and session fixation attacks.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateCorrelationCookie : IOpenIddictClientHandler<ProcessAuthenticationContext>
@ -301,10 +309,10 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
/// <summary>
/// Contains the logic responsible for comparing the current request URL to the redirect_uri stored in the state token.
/// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateRedirectUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
public class ValidateEndpointUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -313,7 +321,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRedirectUri>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -333,10 +341,29 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Try to resolve the original redirect_uri from the state token. If it cannot be resolved,
// this likely means the authorization request was sent without a redirect_uri attached.
if (!Uri.TryCreate(context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
UriKind.Absolute, out Uri? address))
// Resolve the endpoint type allowed to be used with the state token.
if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType),
ignoreCase: true, out OpenIddictClientEndpointType type))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0340));
}
// Resolve the endpoint URI from either the redirect_uri or post_logout_redirect_uri
// depending on the type of endpoint meant to be used with the specified state token.
var value = type switch
{
OpenIddictClientEndpointType.PostLogoutRedirection =>
context.StateTokenPrincipal.GetClaim(Claims.Private.PostLogoutRedirectUri),
OpenIddictClientEndpointType.Redirection =>
context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0340))
};
// If the endpoint URI cannot be resolved, this likely means the authorization or
// logout request was sent without a redirect_uri/post_logout_redirect_uri attached.
if (string.IsNullOrEmpty(value))
{
return default;
}
@ -345,17 +372,18 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host +
request.PathBase + request.Path, UriKind.Absolute);
// Compare the current HTTP request address to the original redirect_uri. If the two don't
// Compare the current HTTP request address to the original endpoint URI. If the two don't
// match, this may indicate a mix-up attack. While the authorization server is expected to
// abort the authorization flow by rejecting the token request that may be eventually sent
// with the original redirect_uri, many servers are known to incorrectly implement this
// redirect_uri validation logic. This check also offers limited protection as it cannot
// with the original endpoint URI, many servers are known to incorrectly implement this
// endpoint URI validation logic. This check also offers limited protection as it cannot
// prevent the authorization code from being leaked to a malicious authorization server.
// By comparing the redirect_uri directly in the client, a first layer of protection is
// By comparing the endpoint URI directly in the client, a first layer of protection is
// provided independently of whether the authorization server will enforce this check.
//
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2
// for more information.
var address = new Uri(value, UriKind.Absolute);
if (uri != new UriBuilder(address) { Query = null }.Uri)
{
context.Reject(
@ -366,9 +394,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers
return default;
}
// Ensure all the query string parameters that were part of the original redirect_uri
// Ensure all the query string parameters that were part of the original endpoint URI
// are present in the current request (parameters that were not part of the original
// redirect_uri are assumed to be authorization response parameters and are ignored).
// endpoint URI are assumed to be authorization response parameters and are ignored).
if (!string.IsNullOrEmpty(address.Query) && QueryHelpers.ParseQuery(address.Query)
.Any(parameter => request.Query[parameter.Key] != parameter.Value))
{
@ -436,6 +464,20 @@ public static partial class OpenIddictClientAspNetCoreHandlers
context.TargetLinkUri = properties.RedirectUri;
}
// If an identity token hint was specified, attach it to the context.
if (properties.Items.TryGetValue(Properties.IdentityTokenHint, out string? token) &&
!string.IsNullOrEmpty(token))
{
context.IdentityTokenHint = token;
}
// If a login hint was specified, attach it to the context.
if (properties.Items.TryGetValue(Properties.LoginHint, out string? hint) &&
!string.IsNullOrEmpty(hint))
{
context.LoginHint = hint;
}
// Preserve the host properties in the principal.
if (properties.Items.Count is not 0)
{
@ -470,11 +512,11 @@ public static partial class OpenIddictClientAspNetCoreHandlers
/// protection against state token injection, forged requests and session fixation attacks.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class GenerateCorrelationCookie : IOpenIddictClientHandler<ProcessChallengeContext>
public class GenerateLoginCorrelationCookie : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public GenerateCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
public GenerateLoginCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -483,8 +525,8 @@ public static partial class OpenIddictClientAspNetCoreHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenGenerated>()
.UseSingletonHandler<GenerateCorrelationCookie>()
.AddFilter<RequireLoginStateTokenGenerated>()
.UseSingletonHandler<GenerateLoginCorrelationCookie>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -538,6 +580,174 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-out parameters stored in the ASP.NET
/// Core authentication properties specified by the application that triggered the sign-out operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ResolveHostSignOutParameters : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostSignOutParameters>()
.SetOrder(ValidateSignOutDemand.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// If an issuer was explicitly set, update the sign-out context to use it.
if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{
// Ensure the issuer set by the application is a valid absolute URI.
if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0306));
}
context.Issuer = uri;
}
// If a return URL was specified, use it as the target_link_uri claim.
if (!string.IsNullOrEmpty(properties.RedirectUri))
{
context.TargetLinkUri = properties.RedirectUri;
}
// If an identity token hint was specified, attach it to the context.
if (properties.Items.TryGetValue(Properties.IdentityTokenHint, out string? token) &&
!string.IsNullOrEmpty(token))
{
context.IdentityTokenHint = token;
}
// If a login hint was specified, attach it to the context.
if (properties.Items.TryGetValue(Properties.LoginHint, out string? hint) &&
!string.IsNullOrEmpty(hint))
{
context.LoginHint = hint;
}
// Preserve the host properties in the principal.
if (properties.Items.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Items);
}
foreach (var parameter in properties.Parameters)
{
context.Parameters[parameter.Key] = parameter.Value switch
{
OpenIddictParameter value => value,
JsonElement value => new OpenIddictParameter(value),
bool value => new OpenIddictParameter(value),
int value => new OpenIddictParameter(value),
long value => new OpenIddictParameter(value),
string value => new OpenIddictParameter(value),
string[] value => new OpenIddictParameter(value),
#if SUPPORTS_JSON_NODES
JsonNode value => new OpenIddictParameter(value),
#endif
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0115))
};
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and denial of service attacks.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class GenerateLogoutCorrelationCookie : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public GenerateLogoutCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<GenerateLogoutCorrelationCookie>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: using a correlation cookie serves as an injection/antiforgery protection as the request
// will always be rejected if a cookie corresponding to the request forgery protection claim
// persisted in the state token cannot be found. This protection is considered essential
// in OpenIddict and cannot be disabled via the options. Applications that prefer implementing
// a different protection strategy can set the request forgery protection claim to null or
// remove this handler from the handlers list and add a custom one using a different approach.
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
return default;
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetHttpRequest()?.HttpContext.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Resolve the cookie builder from the OWIN integration options.
var builder = _options.CurrentValue.CookieBuilder;
// Unless a value was explicitly set in the options, use the expiration date
// of the state token principal as the expiration date of the correlation cookie.
var options = builder.Build(response.HttpContext);
options.Expires ??= context.StateTokenPrincipal.GetExpirationDate();
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
var name = new StringBuilder(builder.Name)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.ToString();
// Add the correlation cookie to the response headers.
response.Cookies.Append(name, "v1", options);
return default;
}
}
/// <summary>
/// Contains the logic responsible for enabling the pass-through mode for the received request.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.

8
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs

@ -13,6 +13,14 @@ namespace OpenIddict.Client.AspNetCore;
/// </summary>
public class OpenIddictClientAspNetCoreOptions : AuthenticationSchemeOptions
{
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the post-logout redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
public bool EnablePostLogoutRedirectionEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.

10
src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs

@ -47,6 +47,16 @@ public class OpenIddictClientOwinBuilder
return this;
}
/// <summary>
/// Enables the pass-through mode for the OpenID Connect post-logout redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
/// <returns>The <see cref="OpenIddictClientOwinBuilder"/>.</returns>
public OpenIddictClientOwinBuilder EnablePostLogoutRedirectionEndpointPassthrough()
=> Configure(options => options.EnablePostLogoutRedirectionEndpointPassthrough = true);
/// <summary>
/// Enables the pass-through mode for the OpenID Connect redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.

6
src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs

@ -23,9 +23,11 @@ public static class OpenIddictClientOwinConstants
{
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_id_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_identity_token_principal";
public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_id_token_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal";
public const string IdentityTokenHint = ".identity_token_hint";
public const string LoginHint = ".login_hint";
public const string Issuer = ".issuer";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";

1
src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs

@ -43,6 +43,7 @@ public static class OpenIddictClientOwinExtensions
// Register the built-in filters used by the default OpenIddict OWIN client event handlers.
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireOwinRequest>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectionEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireRedirectionEndpointPassthroughEnabled>();
// Register the option initializer used by the OpenIddict OWIN client integration services.

44
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -166,6 +166,8 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddictClien
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal),
OpenIddictClientEndpointType.PostLogoutRedirection => context.StateTokenPrincipal,
_ => null
};
@ -316,5 +318,47 @@ public class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddictClien
throw new InvalidOperationException(SR.GetResourceString(SR.ID0111));
}
}
var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
if (signout is not null)
{
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0315));
transaction.Properties[typeof(AuthenticationProperties).FullName!] = signout.Properties ?? new AuthenticationProperties();
var context = new ProcessSignOutContext(transaction)
{
Principal = new ClaimsPrincipal(new ClaimsIdentity()),
Request = new OpenIddictRequest()
};
await _dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
else if (context.IsRejected)
{
var notification = new ProcessErrorContext(transaction)
{
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0111));
}
}
}
}

24
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs

@ -18,7 +18,29 @@ public static class OpenIddictClientOwinHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the authorization endpoint.
/// pass-through mode was not enabled for the post-logout redirection endpoint.
/// </summary>
public class RequirePostLogoutRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientOwinOptions> _options;
public RequirePostLogoutRedirectionEndpointPassthroughEnabled(IOptionsMonitor<OpenIddictClientOwinOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(_options.CurrentValue.EnablePostLogoutRedirectionEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the redirection endpoint.
/// </summary>
public class RequireRedirectionEndpointPassthroughEnabled : IOpenIddictClientHandlerFilter<BaseContext>
{

93
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Session.cs

@ -0,0 +1,93 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using Owin;
namespace OpenIddict.Client.Owin;
public static partial class OpenIddictClientOwinHandlers
{
public static class Session
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Session request processing:
*/
ProcessQueryRequest.Descriptor,
/*
* Post-logout redirection request extraction:
*/
ExtractGetOrPostRequest<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
/*
* Post-logout redirection request handling:
*/
EnablePassthroughMode<HandlePostLogoutRedirectionRequestContext, RequirePostLogoutRedirectionEndpointPassthroughEnabled>.Descriptor,
/*
* Post-logout redirection response handling:
*/
AttachHttpResponseCode<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyPostLogoutRedirectionResponseContext, RequirePostLogoutRedirectionEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible for processing authorization requests using 302 redirects.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ProcessQueryRequest : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyLogoutRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessQueryRequest>()
.SetOrder(50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyLogoutRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
var location = context.EndSessionEndpoint;
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var (key, value) in
from parameter in context.Request.GetParameters()
let values = (string?[]?) parameter.Value
where values is not null
from value in values
where !string.IsNullOrEmpty(value)
select (parameter.Key, Value: value))
{
location = WebUtilities.AddQueryString(location, key, value);
}
response.Redirect(location);
context.HandleRequest();
return default;
}
}
}
}

283
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -33,14 +33,21 @@ public static partial class OpenIddictClientOwinHandlers
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ValidateRedirectUri.Descriptor,
ValidateEndpointUri.Descriptor,
/*
* Challenge processing:
*/
GenerateCorrelationCookie.Descriptor,
ResolveHostChallengeParameters.Descriptor)
.AddRange(Authentication.DefaultHandlers);
ResolveHostChallengeParameters.Descriptor,
GenerateLoginCorrelationCookie.Descriptor,
/*
* Sign-out processing:
*/
ResolveHostSignOutParameters.Descriptor,
GenerateLogoutCorrelationCookie.Descriptor)
.AddRange(Authentication.DefaultHandlers)
.AddRange(Session.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for inferring the endpoint type from the request address.
@ -75,8 +82,9 @@ public static partial class OpenIddictClientOwinHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
context.EndpointType =
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection :
OpenIddictClientEndpointType.Unknown;
Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection :
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection :
OpenIddictClientEndpointType.Unknown;
return default;
@ -309,10 +317,10 @@ public static partial class OpenIddictClientOwinHandlers
}
/// <summary>
/// Contains the logic responsible for comparing the current request URL to the redirect_uri stored in the state token.
/// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateRedirectUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
public class ValidateEndpointUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -321,7 +329,7 @@ public static partial class OpenIddictClientOwinHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRedirectUri>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -341,10 +349,29 @@ public static partial class OpenIddictClientOwinHandlers
var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Try to resolve the original redirect_uri from the state token. If it cannot be resolved,
// this likely means the authorization request was sent without a redirect_uri attached.
if (!Uri.TryCreate(context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
UriKind.Absolute, out Uri? address))
// Resolve the endpoint type allowed to be used with the state token.
if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType),
ignoreCase: true, out OpenIddictClientEndpointType type))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0340));
}
// Resolve the endpoint address from either the redirect_uri or post_logout_redirect_uri
// depending on the type of endpoint allowed to receive the specified state token.
var value = type switch
{
OpenIddictClientEndpointType.PostLogoutRedirection =>
context.StateTokenPrincipal.GetClaim(Claims.Private.PostLogoutRedirectUri),
OpenIddictClientEndpointType.Redirection =>
context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0340))
};
// If the endpoint URI cannot be resolved, this likely means the authorization or
// logout request was sent without a redirect_uri/post_logout_redirect_uri attached.
if (string.IsNullOrEmpty(value))
{
return default;
}
@ -353,17 +380,18 @@ public static partial class OpenIddictClientOwinHandlers
var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host +
request.PathBase + request.Path, UriKind.Absolute);
// Compare the current HTTP request address to the original redirect_uri. If the two don't
// Compare the current HTTP request address to the original endpoint URI. If the two don't
// match, this may indicate a mix-up attack. While the authorization server is expected to
// abort the authorization flow by rejecting the token request that may be eventually sent
// with the original redirect_uri, many servers are known to incorrectly implement this
// redirect_uri validation logic. This check also offers limited protection as it cannot
// with the original endpoint URI, many servers are known to incorrectly implement this
// endpoint URI validation logic. This check also offers limited protection as it cannot
// prevent the authorization code from being leaked to a malicious authorization server.
// By comparing the redirect_uri directly in the client, a first layer of protection is
// By comparing the endpoint URI directly in the client, a first layer of protection is
// provided independently of whether the authorization server will enforce this check.
//
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2
// for more information.
var address = new Uri(value, UriKind.Absolute);
if (uri != new UriBuilder(address) { Query = null }.Uri)
{
context.Reject(
@ -374,9 +402,9 @@ public static partial class OpenIddictClientOwinHandlers
return default;
}
// Ensure all the query string parameters that were part of the original redirect_uri
// Ensure all the query string parameters that were part of the original endpoint URI
// are present in the current request (parameters that were not part of the original
// redirect_uri are assumed to be authorization response parameters and are ignored).
// endpoint URI are assumed to be authorization response parameters and are ignored).
if (!string.IsNullOrEmpty(address.Query) && OpenIddictHelpers.ParseQuery(address.Query)
// Note: ignore parameters that only include empty values
// to match the logic used by OWIN for IOwinRequest.Query.
@ -447,6 +475,20 @@ public static partial class OpenIddictClientOwinHandlers
context.TargetLinkUri = properties.RedirectUri;
}
// If an identity token hint was specified, attach it to the context.
if (properties.Dictionary.TryGetValue(Properties.IdentityTokenHint, out string? token) &&
!string.IsNullOrEmpty(token))
{
context.IdentityTokenHint = token;
}
// If a login hint was specified, attach it to the context.
if (properties.Dictionary.TryGetValue(Properties.LoginHint, out string? hint) &&
!string.IsNullOrEmpty(hint))
{
context.LoginHint = hint;
}
// Preserve the host properties in the principal.
if (properties.Dictionary.Count is not 0)
{
@ -500,11 +542,11 @@ public static partial class OpenIddictClientOwinHandlers
/// protection against state token injection, forged requests and session fixation attacks.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class GenerateCorrelationCookie : IOpenIddictClientHandler<ProcessChallengeContext>
public class GenerateLoginCorrelationCookie : IOpenIddictClientHandler<ProcessChallengeContext>
{
private readonly IOptionsMonitor<OpenIddictClientOwinOptions> _options;
public GenerateCorrelationCookie(IOptionsMonitor<OpenIddictClientOwinOptions> options)
public GenerateLoginCorrelationCookie(IOptionsMonitor<OpenIddictClientOwinOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -513,8 +555,8 @@ public static partial class OpenIddictClientOwinHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenGenerated>()
.UseSingletonHandler<GenerateCorrelationCookie>()
.AddFilter<RequireLoginStateTokenGenerated>()
.UseSingletonHandler<GenerateLoginCorrelationCookie>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -576,6 +618,201 @@ public static partial class OpenIddictClientOwinHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the additional sign-out parameters stored in the ASP.NET
/// Core authentication properties specified by the application that triggered the sign-out operation.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ResolveHostSignOutParameters : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostSignOutParameters>()
.SetOrder(ValidateSignOutDemand.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is null)
{
return default;
}
// If an issuer was explicitly set, update the challenge context to use it.
if (properties.Dictionary.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{
// Ensure the issuer set by the application is a valid absolute URI.
if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0306));
}
context.Issuer = uri;
}
// If a return URL was specified, use it as the target_link_uri claim.
if (!string.IsNullOrEmpty(properties.RedirectUri))
{
context.TargetLinkUri = properties.RedirectUri;
}
// If an identity token hint was specified, attach it to the context.
if (properties.Dictionary.TryGetValue(Properties.IdentityTokenHint, out string? token) &&
!string.IsNullOrEmpty(token))
{
context.IdentityTokenHint = token;
}
// If a login hint was specified, attach it to the context.
if (properties.Dictionary.TryGetValue(Properties.LoginHint, out string? hint) &&
!string.IsNullOrEmpty(hint))
{
context.LoginHint = hint;
}
// Preserve the host properties in the principal.
if (properties.Dictionary.Count is not 0)
{
context.Principal.SetClaim(Claims.Private.HostProperties, properties.Dictionary);
}
// Note: unlike ASP.NET Core, Owin's AuthenticationProperties doesn't offer a strongly-typed
// dictionary that allows flowing parameters while preserving their original types. To allow
// returning custom parameters, the OWIN host allows using AuthenticationProperties.Dictionary
// but requires suffixing the properties that are meant to be used as parameters using a special
// suffix that indicates that the property is public and determines its actual representation.
foreach (var property in properties.Dictionary)
{
var (name, value) = property.Key switch
{
// If the property ends with #string, represent it as a string parameter.
string key when key.EndsWith(PropertyTypes.String, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.String.Length),
Value: new OpenIddictParameter(property.Value)),
// If the property ends with #boolean, return it as a boolean parameter.
string key when key.EndsWith(PropertyTypes.Boolean, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Boolean.Length),
Value: new OpenIddictParameter(bool.Parse(property.Value))),
// If the property ends with #integer, return it as an integer parameter.
string key when key.EndsWith(PropertyTypes.Integer, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Integer.Length),
Value: new OpenIddictParameter(long.Parse(property.Value, CultureInfo.InvariantCulture))),
// If the property ends with #json, return it as a JSON parameter.
string key when key.EndsWith(PropertyTypes.Json, StringComparison.OrdinalIgnoreCase) => (
Name: key.Substring(0, key.Length - PropertyTypes.Json.Length),
Value: new OpenIddictParameter(JsonSerializer.Deserialize<JsonElement>(property.Value))),
_ => default
};
if (!string.IsNullOrEmpty(name))
{
context.Parameters[name] = value;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for creating a correlation cookie that serves as a
/// protection against state token injection, forged requests and denial of service attacks.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class GenerateLogoutCorrelationCookie : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOptionsMonitor<OpenIddictClientOwinOptions> _options;
public GenerateLogoutCorrelationCookie(IOptionsMonitor<OpenIddictClientOwinOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<GenerateLogoutCorrelationCookie>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: using a correlation cookie serves as an injection/antiforgery protection as the request
// will always be rejected if a cookie corresponding to the request forgery protection claim
// persisted in the state token cannot be found. This protection is considered essential
// in OpenIddict and cannot be disabled via the options. Applications that prefer implementing
// a different protection strategy can set the request forgery protection claim to null or
// remove this handler from the handlers list and add a custom one using a different approach.
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
return default;
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// This handler only applies to OWIN requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
var name = new StringBuilder(_options.CurrentValue.CookieName)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.ToString();
// Resolve the cookie manager and the cookie options from the OWIN integration options.
var (manager, options) = (
_options.CurrentValue.CookieManager,
_options.CurrentValue.CookieOptions);
// Add the correlation cookie to the response headers.
manager.AppendResponseCookie(response.Context, name, "v1", new CookieOptions
{
Domain = options.Domain,
HttpOnly = options.HttpOnly,
Path = options.Path,
SameSite = options.SameSite,
Secure = options.Secure,
// Use the expiration date of the state token principal
// as the expiration date of the correlation cookie.
Expires = context.StateTokenPrincipal.GetExpirationDate()?.UtcDateTime
});
return default;
}
}
/// <summary>
/// Contains the logic responsible for enabling the pass-through mode for the received request.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.

8
src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs

@ -20,6 +20,14 @@ public class OpenIddictClientOwinOptions : AuthenticationOptions
: base(OpenIddictClientOwinDefaults.AuthenticationType)
=> AuthenticationMode = AuthenticationMode.Passive;
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the post-logout redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
public bool EnablePostLogoutRedirectionEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the redirection endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.

46
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -1028,6 +1028,52 @@ public class OpenIddictClientBuilder
});
}
/// <summary>
/// Sets the relative or absolute URLs associated to the post-logout redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public OpenIddictClientBuilder SetPostLogoutRedirectionEndpointUris(params string[] addresses)
{
if (addresses is null)
{
throw new ArgumentNullException(nameof(addresses));
}
return SetPostLogoutRedirectionEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray());
}
/// <summary>
/// Sets the relative or absolute URLs associated to the post-logout redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public OpenIddictClientBuilder SetPostLogoutRedirectionEndpointUris(params Uri[] addresses)
{
if (addresses is null)
{
throw new ArgumentNullException(nameof(addresses));
}
if (addresses.Any(address => !address.IsWellFormedOriginalString()))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(addresses));
}
if (addresses.Any(address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(SR.FormatID0081("~"), nameof(addresses));
}
return Configure(options =>
{
options.PostLogoutRedirectionEndpointUris.Clear();
options.PostLogoutRedirectionEndpointUris.AddRange(addresses);
});
}
/// <summary>
/// Sets the client assertion token lifetime, after which backchannel requests
/// using an expired state token should be automatically rejected by the server.

7
src/OpenIddict.Client/OpenIddictClientEndpointType.cs

@ -19,5 +19,10 @@ public enum OpenIddictClientEndpointType
/// <summary>
/// Redirection endpoint.
/// </summary>
Redirection = 1
Redirection = 1,
/// <summary>
/// Post-logout redirection endpoint.
/// </summary>
PostLogoutRedirection = 2
}

190
src/OpenIddict.Client/OpenIddictClientEvents.Session.cs

@ -0,0 +1,190 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Security.Claims;
namespace OpenIddict.Client;
public static partial class OpenIddictClientEvents
{
/// <summary>
/// Represents an event called for each request to the logout endpoint to give the user code
/// a chance to manually update the logout request before it is sent to the identity provider.
/// </summary>
public class PrepareLogoutRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareLogoutRequestContext"/> class.
/// </summary>
public PrepareLogoutRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the principal containing the claims stored in the state object.
/// </summary>
public ClaimsPrincipal StatePrincipal { get; set; } = new ClaimsPrincipal(new ClaimsIdentity());
}
/// <summary>
/// Represents an event called for each request to the logout endpoint
/// to give the user code a chance to manually send the logout request.
/// </summary>
public class ApplyLogoutRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareLogoutRequestContext"/> class.
/// </summary>
public ApplyLogoutRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
public string EndSessionEndpoint { get; set; } = null!;
}
/// <summary>
/// Represents an event called for each request to the post-logout redirection endpoint to give the user code
/// a chance to manually extract the redirection request from the ambient HTTP context.
/// </summary>
public class ExtractPostLogoutRedirectionRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractPostLogoutRedirectionRequestContext"/> class.
/// </summary>
public ExtractPostLogoutRedirectionRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request or <see langword="null"/> if it was extracted yet.
/// </summary>
public OpenIddictRequest? Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each request to the post-logout redirection endpoint
/// to determine if the request is valid and should continue to be processed.
/// </summary>
public class ValidatePostLogoutRedirectionRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ValidatePostLogoutRedirectionRequestContext"/> class.
/// </summary>
public ValidatePostLogoutRedirectionRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the security principal extracted from the identity token,
/// if applicable to the current redirection request. If no identity token
/// is available at the validation stage, a token request will typically be
/// sent to retrieve a complete set of tokens (e.g logout code flow).
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the state token.
/// </summary>
public ClaimsPrincipal? StateTokenPrincipal { get; set; }
}
/// <summary>
/// Represents an event called for each validated redirection request
/// to allow the user code to decide how the request should be handled.
/// </summary>
public class HandlePostLogoutRedirectionRequestContext : BaseValidatingTicketContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandlePostLogoutRedirectionRequestContext"/> class.
/// </summary>
public HandlePostLogoutRedirectionRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets the additional parameters returned to the client application.
/// </summary>
public Dictionary<string, OpenIddictParameter> Parameters { get; private set; }
= new(StringComparer.Ordinal);
}
/// <summary>
/// Represents an event called before the redirection response is returned to the caller.
/// </summary>
public class ApplyPostLogoutRedirectionResponseContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyPostLogoutRedirectionResponseContext"/> class.
/// </summary>
public ApplyPostLogoutRedirectionResponseContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request, or <see langword="null"/> if it couldn't be extracted.
/// </summary>
public OpenIddictRequest? Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response!;
set => Transaction.Response = value;
}
}
}

111
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -760,6 +760,17 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? TargetLinkUri { get; set; }
/// <summary>
/// Gets or sets the optional identity token hint that will
/// be sent to the authorization server, if applicable.
/// </summary>
public string? IdentityTokenHint { get; set; }
/// <summary>
/// Gets or sets the optional login hint that will be sent to the authorization server, if applicable.
/// </summary>
public string? LoginHint { get; set; }
/// <summary>
/// Gets the set of scopes that will be requested to the authorization server.
/// </summary>
@ -794,4 +805,104 @@ public static partial class OpenIddictClientEvents
/// </summary>
public ClaimsPrincipal? StateTokenPrincipal { get; set; }
}
/// <summary>
/// Represents an event called when processing a sign-out response.
/// </summary>
public class ProcessSignOutContext : BaseValidatingTicketContext
{
/// <summary>
/// Creates a new instance of the <see cref="ProcessSignOutContext"/> class.
/// </summary>
public ProcessSignOutContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response!;
set => Transaction.Response = value;
}
/// <summary>
/// Gets or sets the client identifier that will be used for the sign-out demand.
/// </summary>
public string? ClientId { get; set; }
/// <summary>
/// Gets or sets the post-logout redirection endpoint that
/// will be used for the sign-out demand, if applicable.
/// </summary>
public string? PostLogoutRedirectUri { get; set; }
/// <summary>
/// Gets or sets the optional identity token hint that will
/// be sent to the authorization server, if applicable.
/// </summary>
public string? IdentityTokenHint { get; set; }
/// <summary>
/// Gets or sets the optional login hint that will be sent to the authorization server, if applicable.
/// </summary>
public string? LoginHint { get; set; }
/// <summary>
/// Gets or sets the optional return URL that will be stored in the state token, if applicable.
/// </summary>
public string? TargetLinkUri { get; set; }
/// <summary>
/// Gets or sets the request forgery protection that will be stored in the state token, if applicable.
/// Note: this value MUST NOT be user-defined or extracted from any request and MUST be random
/// (generated by a random number generator suitable for cryptographic operations).
/// </summary>
public string? RequestForgeryProtection { get; set; }
/// <summary>
/// Gets the additional parameters returned to caller.
/// </summary>
public Dictionary<string, OpenIddictParameter> Parameters { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets a boolean indicating whether a state token
/// should be generated (and optionally included in the request).
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
/// </summary>
public bool GenerateStateToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated
/// state token should be included as part of the request.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
/// </summary>
public bool IncludeStateToken { get; set; }
/// <summary>
/// Gets or sets the generated state token, if applicable.
/// The state token will only be returned if
/// <see cref="IncludeStateToken"/> is set to <see langword="true"/>.
/// </summary>
public string? StateToken { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the state token, if applicable.
/// </summary>
public ClaimsPrincipal? StateTokenPrincipal { get; set; }
}
}

4
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -45,10 +45,12 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireInteractiveGrantType>();
builder.Services.TryAddSingleton<RequireLoginStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireLogoutStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireStateTokenPrincipal>();
builder.Services.TryAddSingleton<RequireStateTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();

54
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -173,9 +173,41 @@ public static class OpenIddictClientHandlerFilters
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a redirection request.
/// Represents a filter that excludes the associated handlers if no login state token is generated.
/// </summary>
public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter<BaseContext>
public class RequireLoginStateTokenGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateStateToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no logout state token is generated.
/// </summary>
public class RequireLogoutStateTokenGenerated : IOpenIddictClientHandlerFilter<ProcessSignOutContext>
{
public ValueTask<bool> IsActiveAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateStateToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a post-logout redirection request.
/// </summary>
public class RequirePostLogoutRedirectionRequest : IOpenIddictClientHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
@ -184,39 +216,39 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(context.EndpointType is OpenIddictClientEndpointType.Redirection);
return new(context.EndpointType is OpenIddictClientEndpointType.PostLogoutRedirection);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no refresh token is validated.
/// Represents a filter that excludes the associated handlers if the request is not a redirection request.
/// </summary>
public class RequireRefreshTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateRefreshToken);
return new(context.EndpointType is OpenIddictClientEndpointType.Redirection);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no state token is generated.
/// Represents a filter that excludes the associated handlers if no refresh token is validated.
/// </summary>
public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
public class RequireRefreshTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateStateToken);
return new(context.ValidateRefreshToken);
}
}

47
src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs

@ -23,6 +23,7 @@ public static partial class OpenIddictClientHandlers
ValidateIssuer.Descriptor,
ExtractAuthorizationEndpoint.Descriptor,
ExtractCryptographyEndpoint.Descriptor,
ExtractLogoutEndpoint.Descriptor,
ExtractTokenEndpoint.Descriptor,
ExtractUserinfoEndpoint.Descriptor,
ExtractGrantTypes.Descriptor,
@ -89,6 +90,7 @@ public static partial class OpenIddictClientHandlers
{
// The following parameters MUST be formatted as unique strings:
Metadata.AuthorizationEndpoint or
Metadata.EndSessionEndpoint or
Metadata.Issuer or
Metadata.JwksUri or
Metadata.TokenEndpoint or
@ -295,6 +297,49 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting the logout endpoint address from the discovery document.
/// </summary>
public class ExtractLogoutEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractLogoutEndpoint>()
.SetOrder(ExtractCryptographyEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var address = (string?) context.Response[Metadata.EndSessionEndpoint];
if (!string.IsNullOrEmpty(address))
{
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
context.Reject(
error: Errors.ServerError,
description: SR.FormatID2100(Metadata.EndSessionEndpoint),
uri: SR.FormatID8000(SR.ID2100));
return default;
}
context.Configuration.EndSessionEndpoint = uri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting the token endpoint address from the discovery document.
/// </summary>
@ -306,7 +351,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractTokenEndpoint>()
.SetOrder(ExtractCryptographyEndpoint.Descriptor.Order + 1_000)
.SetOrder(ExtractLogoutEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

450
src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs

@ -0,0 +1,450 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Client;
public static partial class OpenIddictClientHandlers
{
public static class Session
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Logout request top-level processing:
*/
PrepareLogoutRequest.Descriptor,
ApplyLogoutRequest.Descriptor,
/*
* Logout request processing:
*/
AttachLogoutEndpoint.Descriptor,
/*
* Post-logout redirection request top-level processing:
*/
ExtractPostLogoutRedirectionRequest.Descriptor,
ValidatePostLogoutRedirectionRequest.Descriptor,
HandlePostLogoutRedirectionRequest.Descriptor,
ApplyPostLogoutRedirectionResponse<ProcessErrorContext>.Descriptor,
ApplyPostLogoutRedirectionResponse<ProcessRequestContext>.Descriptor,
/*
* Post-logout redirection request validation:
*/
ValidateTokens.Descriptor);
/// <summary>
/// Contains the logic responsible for preparing authorization requests and invoking the corresponding event handlers.
/// </summary>
public class PrepareLogoutRequest : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public PrepareLogoutRequest(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseScopedHandler<PrepareLogoutRequest>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new PrepareLogoutRequestContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
}
}
/// <summary>
/// Contains the logic responsible for applying authorization requests and invoking the corresponding event handlers.
/// </summary>
public class ApplyLogoutRequest : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ApplyLogoutRequest(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseScopedHandler<ApplyLogoutRequest>()
.SetOrder(PrepareLogoutRequest.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new ApplyLogoutRequestContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
}
}
/// <summary>
/// Contains the logic responsible for attaching the address of the authorization request to the request.
/// </summary>
public class AttachLogoutEndpoint : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyLogoutRequestContext>()
.UseSingletonHandler<AttachLogoutEndpoint>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ApplyLogoutRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Ensure the end session endpoint is present and is a valid absolute URL.
if (context.Configuration.EndSessionEndpoint is not { IsAbsoluteUri: true } ||
!context.Configuration.EndSessionEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.EndSessionEndpoint));
}
context.EndSessionEndpoint = context.Configuration.EndSessionEndpoint.AbsoluteUri;
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting redirection requests and invoking the corresponding event handlers.
/// </summary>
public class ExtractPostLogoutRedirectionRequest : IOpenIddictClientHandler<ProcessRequestContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ExtractPostLogoutRedirectionRequest(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequirePostLogoutRedirectionRequest>()
.UseScopedHandler<ExtractPostLogoutRedirectionRequest>()
.SetOrder(100_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new ExtractPostLogoutRedirectionRequestContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
if (notification.Request is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0302));
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6199), notification.Request);
}
}
/// <summary>
/// Contains the logic responsible for validating redirection requests and invoking the corresponding event handlers.
/// </summary>
public class ValidatePostLogoutRedirectionRequest : IOpenIddictClientHandler<ProcessRequestContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ValidatePostLogoutRedirectionRequest(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequirePostLogoutRedirectionRequest>()
.UseScopedHandler<ValidatePostLogoutRedirectionRequest>()
.SetOrder(ExtractPostLogoutRedirectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new ValidatePostLogoutRedirectionRequestContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6200));
}
}
/// <summary>
/// Contains the logic responsible for handling redirection requests and invoking the corresponding event handlers.
/// </summary>
public class HandlePostLogoutRedirectionRequest : IOpenIddictClientHandler<ProcessRequestContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public HandlePostLogoutRedirectionRequest(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequirePostLogoutRedirectionRequest>()
.UseScopedHandler<HandlePostLogoutRedirectionRequest>()
.SetOrder(ValidatePostLogoutRedirectionRequest.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new HandlePostLogoutRedirectionRequestContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6201));
}
}
/// <summary>
/// Contains the logic responsible for processing redirection responses and invoking the corresponding event handlers.
/// </summary>
public class ApplyPostLogoutRedirectionResponse<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ApplyPostLogoutRedirectionResponse(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequirePostLogoutRedirectionRequest>()
.UseScopedHandler<ApplyPostLogoutRedirectionResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new ApplyPostLogoutRedirectionResponseContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0303));
}
}
/// <summary>
/// Contains the logic responsible for rejecting redirection requests that don't
/// specify a valid access token, authorization code, identity token or state token.
/// </summary>
public class ValidateTokens : IOpenIddictClientHandler<ValidatePostLogoutRedirectionRequestContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ValidateTokens(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidatePostLogoutRedirectionRequestContext>()
.UseScopedHandler<ValidateTokens>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidatePostLogoutRedirectionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new ProcessAuthenticationContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
// Attach the security principals extracted from the tokens to the validation context.
context.Principal = notification.FrontchannelIdentityTokenPrincipal;
context.StateTokenPrincipal = notification.StateTokenPrincipal;
}
}
}
}

574
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -31,6 +31,7 @@ public static partial class OpenIddictClientHandlers
ValidateRequiredStateToken.Descriptor,
ValidateStateToken.Descriptor,
RedeemStateTokenEntry.Descriptor,
ValidateStateTokenEndpointType.Descriptor,
ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor,
ValidateFrontchannelErrorParameters.Descriptor,
@ -90,7 +91,7 @@ public static partial class OpenIddictClientHandlers
* Challenge processing:
*/
ValidateChallengeDemand.Descriptor,
ResolveClientRegistration.Descriptor,
ResolveClientRegistrationFromChallengeContext.Descriptor,
AttachGrantType.Descriptor,
EvaluateGeneratedChallengeTokens.Descriptor,
AttachResponseType.Descriptor,
@ -105,6 +106,21 @@ public static partial class OpenIddictClientHandlers
ValidateRedirectUriParameter.Descriptor,
GenerateStateToken.Descriptor,
AttachChallengeParameters.Descriptor,
AttachCustomChallengeParameters.Descriptor,
/*
* Sign-out processing:
*/
ValidateSignOutDemand.Descriptor,
ResolveClientRegistrationFromSignOutContext.Descriptor,
AttachOptionalClientId.Descriptor,
AttachPostLogoutRedirectUri.Descriptor,
EvaluateGeneratedLogoutTokens.Descriptor,
AttachLogoutRequestForgeryProtection.Descriptor,
PrepareLogoutStateTokenPrincipal.Descriptor,
GenerateLogoutStateToken.Descriptor,
AttachSignOutParameters.Descriptor,
AttachCustomSignOutParameters.Descriptor,
/*
* Error processing:
@ -115,6 +131,7 @@ public static partial class OpenIddictClientHandlers
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Protection.DefaultHandlers)
.AddRange(Session.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
/// <summary>
@ -140,13 +157,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Authentication demands can be triggered from the redirection endpoint
// to handle authorization callbacks but also from unknown endpoints
// Authentication demands can be triggered from the redirection endpoints
// to handle authorization/logout callbacks but also from unknown endpoints
// when using the refresh token grant, to perform a token refresh dance.
switch (context.EndpointType)
{
case OpenIddictClientEndpointType.Redirection:
case OpenIddictClientEndpointType.PostLogoutRedirection:
break;
case OpenIddictClientEndpointType.Unknown:
@ -224,6 +242,13 @@ public static partial class OpenIddictClientHandlers
// chosen authorization server), the state is always considered required at this point.
OpenIddictClientEndpointType.Redirection => (true, true, true),
// While the OpenID Connect RP-initiated logout specification doesn't require sending
// a state as part of logout requests, the identity provider MUST return the state
// if one was initially specified. Since OpenIddict always sends a state (used as a
// way to mitigate CSRF attacks and store per-logout values like the identity of the
// chosen authorization server), the state is always considered required at this point.
OpenIddictClientEndpointType.PostLogoutRedirection => (true, true, true),
_ => (false, false, false)
};
@ -256,8 +281,8 @@ public static partial class OpenIddictClientHandlers
context.StateToken = context.EndpointType switch
{
OpenIddictClientEndpointType.Redirection when context.ExtractStateToken
=> context.Request.State,
OpenIddictClientEndpointType.Redirection or OpenIddictClientEndpointType.PostLogoutRedirection
when context.ExtractStateToken => context.Request.State,
_ => null
};
@ -433,6 +458,56 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for ensuring the resolved state
/// token is suitable for the requested authentication demand.
/// </summary>
public class ValidateStateTokenEndpointType : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ValidateStateTokenEndpointType>()
.SetOrder(RedeemStateTokenEntry.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the endpoint type allowed to be used with the state token.
if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType),
ignoreCase: true, out OpenIddictClientEndpointType type))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0340));
}
// Reject the authentication demand if the expected endpoint type doesn't
// match the current endpoint type as it may indicate a mix-up attack (e.g a
// state token created for a logout operation was used for a login operation).
if (type != context.EndpointType)
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2142),
uri: SR.FormatID8000(SR.ID2142));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the client registration
/// based on the authorization server identity stored in the state token.
@ -446,7 +521,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ResolveClientRegistrationFromStateToken>()
.SetOrder(RedeemStateTokenEntry.Descriptor.Order + 1_000)
.SetOrder(ValidateStateTokenEndpointType.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -636,6 +711,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ResolveGrantTypeFromStateToken>()
.SetOrder(ValidateFrontchannelErrorParameters.Descriptor.Order + 1_000)
@ -689,6 +765,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ResolveResponseTypeFromStateToken>()
.SetOrder(ResolveGrantTypeFromStateToken.Descriptor.Order + 1_000)
@ -3276,6 +3353,11 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0006));
}
// If an explicit grant type was specified, ensure it is supported by OpenIddict.
if (!string.IsNullOrEmpty(context.GrantType) &&
context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit))
@ -3300,14 +3382,14 @@ public static partial class OpenIddictClientHandlers
/// <summary>
/// Contains the logic responsible for resolving the client registration applicable to the challenge demand.
/// </summary>
public class ResolveClientRegistration : IOpenIddictClientHandler<ProcessChallengeContext>
public class ResolveClientRegistrationFromChallengeContext : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<ResolveClientRegistration>()
.UseSingletonHandler<ResolveClientRegistrationFromChallengeContext>()
.SetOrder(ValidateChallengeDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -3353,7 +3435,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachGrantType>()
.SetOrder(ResolveClientRegistration.Descriptor.Order + 1_000)
.SetOrder(ResolveClientRegistrationFromChallengeContext.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -4062,7 +4144,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireStateTokenGenerated>()
.AddFilter<RequireLoginStateTokenGenerated>()
.UseSingletonHandler<PrepareStateTokenPrincipal>()
.SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -4136,6 +4218,11 @@ public static partial class OpenIddictClientHandlers
// help mitigate downgrade attacks (e.g authorization code flow -> implicit flow).
principal.SetClaim(Claims.Private.ResponseType, context.ResponseType);
// Store the type of endpoint allowed to receive the generated state token.
principal.SetClaim(Claims.Private.EndpointType, Enum.GetName(
typeof(OpenIddictClientEndpointType),
OpenIddictClientEndpointType.Redirection)!.ToLowerInvariant());
// Store the optional redirect_uri to allow sending it as part of the token request.
principal.SetClaim(Claims.Private.RedirectUri, context.RedirectUri);
@ -4174,7 +4261,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireStateTokenGenerated>()
.AddFilter<RequireLoginStateTokenGenerated>()
.UseScopedHandler<GenerateStateToken>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -4304,11 +4391,42 @@ public static partial class OpenIddictClientHandlers
context.Request.CodeChallenge = context.CodeChallenge;
context.Request.CodeChallengeMethod = context.CodeChallengeMethod;
context.Request.IdTokenHint = context.IdentityTokenHint;
context.Request.LoginHint = context.LoginHint;
if (context.IncludeStateToken)
{
context.Request.State = context.StateToken;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the challenge response.
/// </summary>
public class AttachCustomChallengeParameters : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachCustomChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
{
foreach (var parameter in context.Parameters)
@ -4321,6 +4439,440 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for ensuring that the sign-out demand
/// is compatible with the type of the endpoint that handled the request.
/// </summary>
public class ValidateSignOutDemand : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<ValidateSignOutDemand>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0024));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the client registration applicable to the sign-out demand.
/// </summary>
public class ResolveClientRegistrationFromSignOutContext : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<ResolveClientRegistrationFromSignOutContext>()
.SetOrder(ValidateSignOutDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration = context.Options.Registrations.Find(
registration => registration.Issuer == context.Issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
// Resolve and attach the server configuration to the context.
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
context.Configuration = configuration;
}
}
/// <summary>
/// Contains the logic responsible for attaching the client identifier to the sign-out request.
/// </summary>
public class AttachOptionalClientId : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachOptionalClientId>()
.SetOrder(ResolveClientRegistrationFromSignOutContext.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: the client_id parameter is optional.
context.ClientId ??= context.Registration.ClientId;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the post_logout_redirect_uri to the sign-out request.
/// </summary>
public class AttachPostLogoutRedirectUri : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachPostLogoutRedirectUri>()
.SetOrder(AttachOptionalClientId.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: the post_logout_redirect_uri parameter is optional.
context.PostLogoutRedirectUri ??= context.Registration.PostLogoutRedirectUri?.AbsoluteUri;
return default;
}
}
/// <summary>
/// Contains the logic responsible for selecting the token types that
/// should be generated and optionally returned in the response.
/// </summary>
public class EvaluateGeneratedLogoutTokens : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<EvaluateGeneratedLogoutTokens>()
.SetOrder(AttachPostLogoutRedirectUri.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
(context.GenerateStateToken, context.IncludeStateToken) = (true, true);
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching a request forgery protection to the authorization request.
/// </summary>
public class AttachLogoutRequestForgeryProtection : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachLogoutRequestForgeryProtection>()
.SetOrder(EvaluateGeneratedLogoutTokens.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Generate a new crypto-secure random identifier that will
// be used as the non-guessable part of the state token.
var data = new byte[256 / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(data);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(data);
#endif
context.RequestForgeryProtection = Base64UrlEncoder.Encode(data);
return default;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the logout state token, if one is going to be returned.
/// </summary>
public class PrepareLogoutStateTokenPrincipal : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<PrepareLogoutStateTokenPrincipal>()
.SetOrder(AttachLogoutRequestForgeryProtection.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Create a new principal containing only the filtered claims.
// Actors identities are also filtered (delegation scenarios).
var principal = context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Other claims are always included in the state token, even private claims.
return true;
});
principal.SetCreationDate(DateTimeOffset.UtcNow);
var lifetime = context.Principal.GetStateTokenLifetime() ?? context.Options.StateTokenLifetime;
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
// Store the identity of the authorization server in the state token
// principal to allow resolving it when handling the post-logout callback.
//
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09
// for more information about this special claim.
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri);
// Store the request forgery protection in the state token so it can be later used to
// ensure the authorization response sent to the redirection endpoint is not forged.
principal.SetClaim(Claims.RequestForgeryProtection, context.RequestForgeryProtection);
// Store the optional return URL in the state token.
principal.SetClaim(Claims.TargetLinkUri, context.TargetLinkUri);
// Store the type of endpoint allowed to receive the generated state token.
principal.SetClaim(Claims.Private.EndpointType, Enum.GetName(
typeof(OpenIddictClientEndpointType),
OpenIddictClientEndpointType.PostLogoutRedirection)!.ToLowerInvariant());
// Store the post_logout_redirect_uri to allow comparing to the actual redirection URL.
principal.SetClaim(Claims.Private.PostLogoutRedirectUri, context.PostLogoutRedirectUri);
context.StateTokenPrincipal = principal;
return default;
}
}
/// <summary>
/// Contains the logic responsible for generating a logout state token for the current sign-out operation.
/// </summary>
public class GenerateLogoutStateToken : IOpenIddictClientHandler<ProcessSignOutContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public GenerateLogoutStateToken(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseScopedHandler<GenerateLogoutStateToken>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new GenerateTokenContext(context.Transaction)
{
CreateTokenEntry = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
Principal = context.StateTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.StateToken
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.StateToken = notification.Token;
}
}
/// <summary>
/// Contains the logic responsible for attaching the appropriate parameters to the sign-out response.
/// </summary>
public class AttachSignOutParameters : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachSignOutParameters>()
.SetOrder(GenerateLogoutStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: while the exact order of the parameters has typically no effect on how requests
// are handled by an authorization server, client_id and post_logout_redirect_uri are
// set first so that they appear early in the URL (when GET requests are used), making
// mistyped values easier to spot when an error is returned by the identity provider.
context.Request.ClientId = context.ClientId;
context.Request.PostLogoutRedirectUri = context.PostLogoutRedirectUri;
context.Request.IdTokenHint = context.IdentityTokenHint;
context.Request.LoginHint = context.LoginHint;
if (context.IncludeStateToken)
{
context.Request.State = context.StateToken;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the sign-out response.
/// </summary>
public class AttachCustomSignOutParameters : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachCustomSignOutParameters>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
{
foreach (var parameter in context.Parameters)
{
context.Response.SetParameter(parameter.Key, parameter.Value);
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting potential errors from the response.
/// </summary>

5
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -82,6 +82,11 @@ public class OpenIddictClientOptions
/// </summary>
public List<Uri> RedirectionEndpointUris { get; } = new();
/// <summary>
/// Gets the absolute and relative URIs associated to the post-logout redirection endpoint.
/// </summary>
public List<Uri> PostLogoutRedirectionEndpointUris { get; } = new();
/// <summary>
/// Gets the static client registrations used by the OpenIddict client services.
/// </summary>

5
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -29,6 +29,11 @@ public class OpenIddictClientRegistration
/// </summary>
public Uri? RedirectUri { get; set; }
/// <summary>
/// Gets or sets the address of the post-logout redirection endpoint that will handle the callback.
/// </summary>
public Uri? PostLogoutRedirectUri { get; set; }
/// <summary>
/// Gets the list of encryption credentials used to create tokens for this client.
/// Multiple credentials can be added to support key rollover, but if X.509 keys

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs

@ -34,7 +34,7 @@ public static class OpenIddictServerAspNetCoreConstants
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
public const string IdentityTokenPrincipal = ".id_token_principal";
public const string IdentityTokenPrincipal = ".identity_token_principal";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string Scope = ".scope";
public const string UserCodePrincipal = ".user_code_principal";

1
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -10,7 +10,6 @@ using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static OpenIddict.Server.OpenIddictServerHandlers.Authentication;
namespace OpenIddict.Server;

Loading…
Cancel
Save