Browse Source

Implement complete WWW-Authenticate response header support

pull/903/head
Kévin Chalet 6 years ago
parent
commit
97dffed124
  1. 18
      samples/Mvc.Client/Controllers/HomeController.cs
  2. 6
      samples/Mvc.Client/Startup.cs
  3. 215
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  4. 26
      samples/Mvc.Server/Controllers/ResourceController.cs
  5. 143
      samples/Mvc.Server/Startup.cs
  6. 2
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  7. 9
      src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
  8. 14
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
  9. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  10. 6
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  11. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  12. 5
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
  13. 4
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs
  14. 3
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
  15. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
  16. 3
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
  17. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  18. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs
  19. 388
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  20. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  21. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  22. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  23. 5
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
  24. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs
  25. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
  26. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
  27. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
  28. 4
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  29. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs
  30. 347
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  31. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  32. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  33. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  34. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  35. 44
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  36. 6
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  37. 2
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs
  38. 71
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  39. 349
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  40. 2
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs
  41. 44
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  42. 357
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  43. 34
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  44. 80
      src/OpenIddict.Validation/OpenIddictValidationHelpers.cs
  45. 6
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  46. 7
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs
  47. 6
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  48. 20
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  49. 4
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
  50. 45
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

18
samples/Mvc.Client/Controllers/HomeController.cs

@ -13,18 +13,13 @@ namespace Mvc.Client.Controllers
{ {
public class HomeController : Controller public class HomeController : Controller
{ {
private readonly HttpClient _client; private readonly IHttpClientFactory _httpClientFactory;
public HomeController(HttpClient client) public HomeController(IHttpClientFactory httpClientFactory)
{ => _httpClientFactory = httpClientFactory;
_client = client;
}
[HttpGet("~/")] [HttpGet("~/")]
public ActionResult Index() public ActionResult Index() => View("Home");
{
return View("Home");
}
[Authorize, HttpPost("~/")] [Authorize, HttpPost("~/")]
public async Task<ActionResult> Index(CancellationToken cancellationToken) public async Task<ActionResult> Index(CancellationToken cancellationToken)
@ -36,10 +31,11 @@ namespace Mvc.Client.Controllers
"Make sure that SaveTokens is set to true in the OIDC options."); "Make sure that SaveTokens is set to true in the OIDC options.");
} }
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:54540/api/message"); using var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:54540/api/message");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request, cancellationToken); using var client = _httpClientFactory.CreateClient();
using var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return View("Home", model: await response.Content.ReadAsStringAsync()); return View("Home", model: await response.Content.ReadAsStringAsync());

6
samples/Mvc.Client/Startup.cs

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -47,6 +46,7 @@ namespace Mvc.Client
options.Scope.Add("email"); options.Scope.Add("email");
options.Scope.Add("roles"); options.Scope.Add("roles");
options.Scope.Add("offline_access"); options.Scope.Add("offline_access");
options.Scope.Add("demo_api");
options.SecurityTokenValidator = new JwtSecurityTokenHandler options.SecurityTokenValidator = new JwtSecurityTokenHandler
{ {
@ -60,9 +60,9 @@ namespace Mvc.Client
options.AccessDeniedPath = "/"; options.AccessDeniedPath = "/";
}); });
services.AddMvc(); services.AddHttpClient();
services.AddSingleton<HttpClient>(); services.AddMvc();
} }
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)

215
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -32,17 +32,20 @@ namespace Mvc.Server
{ {
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager; private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly OpenIddictAuthorizationManager<OpenIddictAuthorization> _authorizationManager; private readonly OpenIddictAuthorizationManager<OpenIddictAuthorization> _authorizationManager;
private readonly OpenIddictScopeManager<OpenIddictScope> _scopeManager;
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController( public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager, OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
OpenIddictAuthorizationManager<OpenIddictAuthorization> authorizationManager, OpenIddictAuthorizationManager<OpenIddictAuthorization> authorizationManager,
OpenIddictScopeManager<OpenIddictScope> scopeManager,
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) UserManager<ApplicationUser> userManager)
{ {
_applicationManager = applicationManager; _applicationManager = applicationManager;
_authorizationManager = authorizationManager; _authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager; _signInManager = signInManager;
_userManager = userManager; _userManager = userManager;
} }
@ -61,25 +64,29 @@ namespace Mvc.Server
// Retrieve the user principal stored in the authentication cookie. // Retrieve the user principal stored in the authentication cookie.
// If it can't be extracted, redirect the user to the login page. // If it can't be extracted, redirect the user to the login page.
var result = await HttpContext.AuthenticateAsync(); var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
if (result == null || !result.Succeeded) if (result == null || !result.Succeeded)
{ {
// If the client application requested promptless authentication, // If the client application requested promptless authentication,
// return an error indicating that the user is not logged in. // return an error indicating that the user is not logged in.
if (request.HasPrompt(Prompts.None)) if (request.HasPrompt(Prompts.None))
{ {
return Forbid(new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." {
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
} }
return Challenge(new AuthenticationProperties return Challenge(
{ authenticationSchemes: IdentityConstants.ApplicationScheme,
RedirectUri = Request.PathBase + Request.Path + QueryString.Create( properties: new AuthenticationProperties
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) {
}); RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
} }
// If prompt=login was specified by the client application, // If prompt=login was specified by the client application,
@ -96,35 +103,41 @@ namespace Mvc.Server
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt))); parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
return Challenge(new AuthenticationProperties return Challenge(
{ authenticationSchemes: IdentityConstants.ApplicationScheme,
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) properties: new AuthenticationProperties
}); {
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
} }
// If a max_age parameter was provided, ensure that the cookie is not too old. // If a max_age parameter was provided, ensure that the cookie is not too old.
// If it's too old, automatically redirect the user agent to the login page. // If it's too old, automatically redirect the user agent to the login page.
if (request.MaxAge != null && result.Properties.IssuedUtc != null && if (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)) DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))
{ {
if (request.HasPrompt(Prompts.None)) if (request.HasPrompt(Prompts.None))
{ {
return Forbid(new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." {
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
} }
return Challenge(new AuthenticationProperties return Challenge(
{ authenticationSchemes: IdentityConstants.ApplicationScheme,
RedirectUri = Request.PathBase + Request.Path + QueryString.Create( properties: new AuthenticationProperties
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList()) {
}); RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
} }
// Retrieve the profile of the logged in user. // Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User) ?? var user = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved."); throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database. // Retrieve the application details from the database.
@ -133,7 +146,7 @@ namespace Mvc.Server
// Retrieve the permanent authorizations associated with the user and the calling client application. // Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync( var authorizations = await _authorizationManager.FindAsync(
subject: User.FindFirst(Claims.Subject)?.Value, subject: await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application), client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid, status : Statuses.Valid,
type : AuthorizationTypes.Permanent, type : AuthorizationTypes.Permanent,
@ -144,12 +157,14 @@ namespace Mvc.Server
// If the consent is external (e.g when authorizations are granted by a sysadmin), // If the consent is external (e.g when authorizations are granted by a sysadmin),
// immediately return an error if no authorization can be found in the database. // immediately return an error if no authorization can be found in the database.
case ConsentTypes.External when !authorizations.Any(): case ConsentTypes.External when !authorizations.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = {
"The logged in user is not allowed to access this client application." [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
// If the consent is implicit or if an authorization was found, // If the consent is implicit or if an authorization was found,
// return an authorization response without displaying the consent form. // return an authorization response without displaying the consent form.
@ -162,7 +177,7 @@ namespace Mvc.Server
// but you may want to allow the user to uncheck specific scopes. // but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes. // For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes()); principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server"); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent // Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes. // for future authorization or token requests containing the same scopes.
@ -171,7 +186,7 @@ namespace Mvc.Server
{ {
authorization = await _authorizationManager.CreateAsync( authorization = await _authorizationManager.CreateAsync(
principal: principal, principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value, subject : await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application), client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent, type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes())); scopes : ImmutableArray.CreateRange(principal.GetScopes()));
@ -190,12 +205,14 @@ namespace Mvc.Server
// if the client application specified prompt=none in the authorization request. // if the client application specified prompt=none in the authorization request.
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None): case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None): case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
return Forbid(new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = {
"Interactive user consent is required.", [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
// In every other case, render the consent form. // In every other case, render the consent form.
default: default:
@ -224,7 +241,7 @@ namespace Mvc.Server
// Retrieve the permanent authorizations associated with the user and the calling client application. // Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync( var authorizations = await _authorizationManager.FindAsync(
subject: User.FindFirst(Claims.Subject)?.Value, subject: await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application), client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid, status : Statuses.Valid,
type : AuthorizationTypes.Permanent, type : AuthorizationTypes.Permanent,
@ -236,12 +253,14 @@ namespace Mvc.Server
switch (await _applicationManager.GetConsentTypeAsync(application)) switch (await _applicationManager.GetConsentTypeAsync(application))
{ {
case ConsentTypes.External when !authorizations.Any(): case ConsentTypes.External when !authorizations.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = {
"The logged in user is not allowed to access this client application." [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
} }
var principal = await _signInManager.CreateUserPrincipalAsync(user); var principal = await _signInManager.CreateUserPrincipalAsync(user);
@ -250,7 +269,7 @@ namespace Mvc.Server
// but you may want to allow the user to uncheck specific scopes. // but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes. // For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes()); principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server"); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent // Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes. // for future authorization or token requests containing the same scopes.
@ -259,7 +278,7 @@ namespace Mvc.Server
{ {
authorization = await _authorizationManager.CreateAsync( authorization = await _authorizationManager.CreateAsync(
principal: principal, principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value, subject : await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application), client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent, type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes())); scopes : ImmutableArray.CreateRange(principal.GetScopes()));
@ -318,8 +337,8 @@ namespace Mvc.Server
// Redisplay the form when the user code is not valid. // Redisplay the form when the user code is not valid.
return View(new VerifyViewModel return View(new VerifyViewModel
{ {
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error), Error = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription) ErrorDescription = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
}); });
} }
@ -341,7 +360,7 @@ namespace Mvc.Server
// but you may want to allow the user to uncheck specific scopes. // but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes. // For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(result.Principal.GetScopes()); principal.SetScopes(result.Principal.GetScopes());
principal.SetResources("resource_server"); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims) foreach (var claim in principal.Claims)
{ {
@ -361,25 +380,22 @@ namespace Mvc.Server
// Redisplay the form when the user code is not valid. // Redisplay the form when the user code is not valid.
return View(new VerifyViewModel return View(new VerifyViewModel
{ {
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error), Error = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription) ErrorDescription = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
}); });
} }
[Authorize, FormValueRequired("submit.Deny")] [Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/verify"), ValidateAntiForgeryToken] [HttpPost("~/connect/verify"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner. // Notify OpenIddict that the authorization grant has been denied by the resource owner.
public IActionResult VerifyDeny() public IActionResult VerifyDeny() => Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
var properties = new AuthenticationProperties properties: new AuthenticationProperties()
{ {
// This property points to the address OpenIddict will automatically // This property points to the address OpenIddict will automatically
// redirect the user to after rejecting the authorization demand. // redirect the user to after rejecting the authorization demand.
RedirectUri = "/" RedirectUri = "/"
}; });
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
#endregion #endregion
#region Logout support for interactive flows like code and implicit #region Logout support for interactive flows like code and implicit
@ -397,14 +413,15 @@ namespace Mvc.Server
// after a successful authentication flow (e.g Google or Facebook). // after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
// Returning a SignOutResult will ask OpenIddict to redirect the user agent // Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application. // to the post_logout_redirect_uri specified by the client application or to
return SignOut(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); // the RedirectUri specified in the authentication properties if none was set.
return SignOut(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties
{
RedirectUri = "/"
});
} }
#endregion #endregion
@ -423,26 +440,26 @@ namespace Mvc.Server
var user = await _userManager.FindByNameAsync(request.Username); var user = await _userManager.FindByNameAsync(request.Username);
if (user == null) if (user == null)
{ {
var properties = new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." {
}); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); }));
} }
// Validate the username/password parameters and ensure the account is not locked out. // Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true); var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded) if (!result.Succeeded)
{ {
var properties = new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid." {
}); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); }));
} }
var principal = await _signInManager.CreateUserPrincipalAsync(user); var principal = await _signInManager.CreateUserPrincipalAsync(user);
@ -451,7 +468,7 @@ namespace Mvc.Server
// but you may want to allow the user to uncheck specific scopes. // but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes. // For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes()); principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server"); principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims) foreach (var claim in principal.Claims)
{ {
@ -474,25 +491,25 @@ namespace Mvc.Server
var user = await _userManager.GetUserAsync(principal); var user = await _userManager.GetUserAsync(principal);
if (user == null) if (user == null)
{ {
var properties = new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." {
}); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); }));
} }
// Ensure the user is still allowed to sign in. // Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) if (!await _signInManager.CanSignInAsync(user))
{ {
var properties = new AuthenticationProperties(new Dictionary<string, string> return Forbid(
{ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, properties: new AuthenticationProperties(new Dictionary<string, string>
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." {
}); [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); }));
} }
foreach (var claim in principal.Claims) foreach (var claim in principal.Claims)

26
samples/Mvc.Server/Controllers/ResourceController.cs

@ -1,9 +1,13 @@
using System.Threading.Tasks; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models; using Mvc.Server.Models;
using OpenIddict.Abstractions;
using OpenIddict.Validation.AspNetCore; using OpenIddict.Validation.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Mvc.Server.Controllers namespace Mvc.Server.Controllers
{ {
@ -13,14 +17,28 @@ namespace Mvc.Server.Controllers
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
public ResourceController(UserManager<ApplicationUser> userManager) public ResourceController(UserManager<ApplicationUser> userManager)
{ => _userManager = userManager;
_userManager = userManager;
}
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("message")] [HttpGet("message")]
public async Task<IActionResult> GetMessage() public async Task<IActionResult> GetMessage()
{ {
// This demo action requires that the client application be granted the "demo_api" scope.
// If it was not granted, a detailed error is returned to the client application to inform it
// that the authorization process must be restarted with the specified scope to access this API.
if (!User.HasScope("demo_api"))
{
return Forbid(
authenticationSchemes: OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictValidationAspNetCoreConstants.Properties.Scope] = "demo_api",
[OpenIddictValidationAspNetCoreConstants.Properties.Error] = Errors.InsufficientScope,
[OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] =
"The 'demo_api' scope is required to perform this action."
}));
}
var user = await _userManager.GetUserAsync(User); var user = await _userManager.GetUserAsync(User);
if (user == null) if (user == null)
{ {

143
samples/Mvc.Server/Startup.cs

@ -11,6 +11,7 @@ using Mvc.Server.Services;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models; using OpenIddict.EntityFrameworkCore.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Mvc.Server namespace Mvc.Server
{ {
@ -46,9 +47,9 @@ namespace Mvc.Server
// which saves you from doing the mapping in your authorization controller. // which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options => services.Configure<IdentityOptions>(options =>
{ {
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name; options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject; options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role; options.ClaimsIdentity.RoleClaimType = Claims.Role;
}); });
services.AddOpenIddict() services.AddOpenIddict()
@ -79,10 +80,8 @@ namespace Mvc.Server
.AllowPasswordFlow() .AllowPasswordFlow()
.AllowRefreshTokenFlow(); .AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes. // Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
options.RegisterScopes(OpenIddictConstants.Scopes.Email, options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
// Register the signing and encryption credentials. // Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate() options.AddDevelopmentEncryptionCertificate()
@ -125,6 +124,8 @@ namespace Mvc.Server
.AddValidation(options => .AddValidation(options =>
{ {
// Configure the audience accepted by this resource server. // Configure the audience accepted by this resource server.
// The value MUST match the audience associated with the
// "demo_api" scope, which is used by ResourceController.
options.AddAudiences("resource_server"); options.AddAudiences("resource_server");
// Import the configuration from the local OpenIddict server instance. // Import the configuration from the local OpenIddict server instance.
@ -179,71 +180,89 @@ namespace Mvc.Server
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync(); await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<OpenIddictApplicationManager<OpenIddictApplication>>(); await RegisterApplicationsAsync(scope.ServiceProvider);
await RegisterScopesAsync(scope.ServiceProvider);
if (await manager.FindByClientIdAsync("mvc") == null) static async Task RegisterApplicationsAsync(IServiceProvider provider)
{ {
var descriptor = new OpenIddictApplicationDescriptor var manager = provider.GetRequiredService<OpenIddictApplicationManager<OpenIddictApplication>>();
if (await manager.FindByClientIdAsync("mvc") == null)
{ {
ClientId = "mvc", await manager.CreateAsync(new OpenIddictApplicationDescriptor
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = OpenIddictConstants.ConsentTypes.Explicit,
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
Permissions =
{ {
OpenIddictConstants.Permissions.Endpoints.Authorization, ClientId = "mvc",
OpenIddictConstants.Permissions.Endpoints.Logout, ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
OpenIddictConstants.Permissions.Endpoints.Token, ConsentType = ConsentTypes.Explicit,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, DisplayName = "MVC client application",
OpenIddictConstants.Permissions.GrantTypes.RefreshToken, PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
OpenIddictConstants.Permissions.Scopes.Email, RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
OpenIddictConstants.Permissions.Scopes.Profile, Permissions =
OpenIddictConstants.Permissions.Scopes.Roles {
}, Permissions.Endpoints.Authorization,
Requirements = Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "demo_api"
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
// To test this sample with Postman, use the following settings:
//
// * Authorization URL: http://localhost:54540/connect/authorize
// * Access token URL: http://localhost:54540/connect/token
// * Client ID: postman
// * Client secret: [blank] (not used with public clients)
// * Scope: openid email profile roles
// * Grant type: authorization code
// * Request access token locally: yes
if (await manager.FindByClientIdAsync("postman") == null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{ {
OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange ClientId = "postman",
} ConsentType = ConsentTypes.Systematic,
}; DisplayName = "Postman",
RedirectUris = { new Uri("urn:postman") },
await manager.CreateAsync(descriptor); Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Device,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.DeviceCode,
Permissions.GrantTypes.Password,
Permissions.GrantTypes.RefreshToken,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles
}
});
}
} }
// To test this sample with Postman, use the following settings: static async Task RegisterScopesAsync(IServiceProvider provider)
//
// * Authorization URL: http://localhost:54540/connect/authorize
// * Access token URL: http://localhost:54540/connect/token
// * Client ID: postman
// * Client secret: [blank] (not used with public clients)
// * Scope: openid email profile roles
// * Grant type: authorization code
// * Request access token locally: yes
if (await manager.FindByClientIdAsync("postman") == null)
{ {
var descriptor = new OpenIddictApplicationDescriptor var manager = provider.GetRequiredService<OpenIddictScopeManager<OpenIddictScope>>();
if (await manager.FindByNameAsync("demo_api") == null)
{ {
ClientId = "postman", await manager.CreateAsync(new OpenIddictScopeDescriptor
ConsentType = OpenIddictConstants.ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris = { new Uri("urn:postman") },
Permissions =
{ {
OpenIddictConstants.Permissions.Endpoints.Authorization, DisplayName = "Demo API access",
OpenIddictConstants.Permissions.Endpoints.Device, Name = "demo_api",
OpenIddictConstants.Permissions.Endpoints.Token, Resources = { "resource_server" }
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, });
OpenIddictConstants.Permissions.GrantTypes.DeviceCode, }
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
}
};
await manager.CreateAsync(descriptor);
} }
} }
} }

2
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -148,6 +148,8 @@ namespace OpenIddict.Abstractions
public const string AuthorizationPending = "authorization_pending"; public const string AuthorizationPending = "authorization_pending";
public const string ConsentRequired = "consent_required"; public const string ConsentRequired = "consent_required";
public const string ExpiredToken = "expired_token"; public const string ExpiredToken = "expired_token";
public const string InsufficientAccess = "insufficient_access";
public const string InsufficientScope = "insufficient_scope";
public const string InteractionRequired = "interaction_required"; public const string InteractionRequired = "interaction_required";
public const string InvalidClient = "invalid_client"; public const string InvalidClient = "invalid_client";
public const string InvalidGrant = "invalid_grant"; public const string InvalidGrant = "invalid_grant";

9
src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs

@ -138,6 +138,15 @@ namespace OpenIddict.Abstractions
set => SetParameter(OpenIddictConstants.Parameters.IdToken, value); set => SetParameter(OpenIddictConstants.Parameters.IdToken, value);
} }
/// <summary>
/// Gets or sets the "realm" parameter.
/// </summary>
public string Realm
{
get => (string) GetParameter(OpenIddictConstants.Parameters.Realm);
set => SetParameter(OpenIddictConstants.Parameters.Realm, value);
}
/// <summary> /// <summary>
/// Gets or sets the "refresh_token" parameter. /// Gets or sets the "refresh_token" parameter.
/// </summary> /// </summary>

14
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
@ -70,10 +71,10 @@ namespace OpenIddict.Server.AspNetCore
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
bool TryValidate(string scheme) static bool TryValidate(IDictionary<string, AuthenticationSchemeBuilder> map, string scheme)
{ {
// If the scheme was not set or if it cannot be found in the map, return true. // If the scheme was not set or if it cannot be found in the map, return true.
if (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder)) if (string.IsNullOrEmpty(scheme) || !map.TryGetValue(scheme, out var builder))
{ {
return true; return true;
} }
@ -81,9 +82,12 @@ namespace OpenIddict.Server.AspNetCore
return builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler); return builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler);
} }
if (!TryValidate(options.DefaultAuthenticateScheme) || !TryValidate(options.DefaultChallengeScheme) || if (!TryValidate(options.SchemeMap, options.DefaultAuthenticateScheme) ||
!TryValidate(options.DefaultForbidScheme) || !TryValidate(options.DefaultScheme) || !TryValidate(options.SchemeMap, options.DefaultChallengeScheme) ||
!TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme)) !TryValidate(options.SchemeMap, options.DefaultForbidScheme) ||
!TryValidate(options.SchemeMap, options.DefaultScheme) ||
!TryValidate(options.SchemeMap, options.DefaultSignInScheme) ||
!TryValidate(options.SchemeMap, options.DefaultSignOutScheme))
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The OpenIddict ASP.NET Core server cannot be used as the default scheme handler.") .AppendLine("The OpenIddict ASP.NET Core server cannot be used as the default scheme handler.")

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

@ -28,6 +28,8 @@ namespace OpenIddict.Server.AspNetCore
public const string Error = ".error"; public const string Error = ".error";
public const string ErrorDescription = ".error_description"; public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri"; public const string ErrorUri = ".error_uri";
public const string Realm = ".realm";
public const string Scope = ".scope";
} }
} }
} }

6
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -122,6 +122,10 @@ namespace OpenIddict.Server.AspNetCore
{ {
context = new ProcessAuthenticationContext(transaction); context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context); await _provider.DispatchAsync(context);
// 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.
transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName, context);
} }
if (context.IsRequestHandled || context.IsRequestSkipped) if (context.IsRequestHandled || context.IsRequestSkipped)
@ -138,7 +142,7 @@ namespace OpenIddict.Server.AspNetCore
[OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri
}); });
return AuthenticateResult.Fail("An unknown error occurred while authenticating the current request.", properties); return AuthenticateResult.Fail("An error occurred while authenticating the current request.", properties);
} }
return AuthenticateResult.Success(new AuthenticationTicket( return AuthenticateResult.Success(new AuthenticationTicket(

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -52,6 +52,8 @@ namespace OpenIddict.Server.AspNetCore
* Authorization response processing: * Authorization response processing:
*/ */
RemoveCachedRequest.Descriptor, RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyAuthorizationResponseContext>.Descriptor,
ProcessFormPostResponse.Descriptor, ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor, ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor, ProcessFragmentResponse.Descriptor,

5
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs

@ -23,6 +23,9 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Device response processing: * Device response processing:
*/ */
AttachHttpResponseCode<ApplyDeviceResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyDeviceResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyDeviceResponseContext>.Descriptor,
ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor, ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor,
/* /*
@ -38,6 +41,8 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Verification response processing: * Verification response processing:
*/ */
AttachHttpResponseCode<ApplyVerificationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyVerificationResponseContext>.Descriptor,
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor, ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyVerificationResponseContext>.Descriptor, ProcessStatusCodePagesErrorResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor, ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,

4
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Discovery.cs

@ -22,6 +22,8 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Configuration response processing: * Configuration response processing:
*/ */
AttachHttpResponseCode<ApplyConfigurationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyConfigurationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyConfigurationResponseContext>.Descriptor, ProcessJsonResponse<ApplyConfigurationResponseContext>.Descriptor,
/* /*
@ -32,6 +34,8 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Cryptography response processing: * Cryptography response processing:
*/ */
AttachHttpResponseCode<ApplyCryptographyResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyCryptographyResponseContext>.Descriptor,
ProcessJsonResponse<ApplyCryptographyResponseContext>.Descriptor); ProcessJsonResponse<ApplyCryptographyResponseContext>.Descriptor);
} }
} }

3
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs

@ -29,6 +29,9 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Token response processing: * Token response processing:
*/ */
AttachHttpResponseCode<ApplyTokenResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyTokenResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyTokenResponseContext>.Descriptor,
ProcessJsonResponse<ApplyTokenResponseContext>.Descriptor); ProcessJsonResponse<ApplyTokenResponseContext>.Descriptor);
} }
} }

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs

@ -23,6 +23,8 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Introspection response processing: * Introspection response processing:
*/ */
AttachHttpResponseCode<ApplyIntrospectionResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyIntrospectionResponseContext>.Descriptor,
ProcessJsonResponse<ApplyIntrospectionResponseContext>.Descriptor); ProcessJsonResponse<ApplyIntrospectionResponseContext>.Descriptor);
} }
} }

3
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs

@ -23,6 +23,9 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Revocation response processing: * Revocation response processing:
*/ */
AttachHttpResponseCode<ApplyRevocationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyRevocationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyRevocationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyRevocationResponseContext>.Descriptor); ProcessJsonResponse<ApplyRevocationResponseContext>.Descriptor);
} }
} }

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -50,6 +50,8 @@ namespace OpenIddict.Server.AspNetCore
* Logout response processing: * Logout response processing:
*/ */
RemoveCachedRequest.Descriptor, RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyLogoutResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyLogoutResponseContext>.Descriptor,
ProcessQueryResponse.Descriptor, ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse<ApplyLogoutResponseContext>.Descriptor, ProcessHostRedirectionResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyLogoutResponseContext>.Descriptor, ProcessStatusCodePagesErrorResponse<ApplyLogoutResponseContext>.Descriptor,

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs

@ -29,6 +29,8 @@ namespace OpenIddict.Server.AspNetCore
/* /*
* Userinfo response processing: * Userinfo response processing:
*/ */
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyUserinfoResponseContext>.Descriptor,
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor); ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
} }
} }

388
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -314,12 +314,14 @@ namespace OpenIddict.Server.AspNetCore
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) && var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
property is AuthenticationProperties properties) if (properties != null)
{ {
context.Response.Error = properties.GetString(Properties.Error); context.Response.Error = properties.GetString(Properties.Error);
context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription); context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription);
context.Response.ErrorUri = properties.GetString(Properties.ErrorUri); context.Response.ErrorUri = properties.GetString(Properties.ErrorUri);
context.Response.Realm = properties.GetString(Properties.Realm);
context.Response.Scope = properties.GetString(Properties.Scope);
} }
return default; return default;
@ -785,8 +787,8 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
} }
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) && var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri)) if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri))
{ {
response.Redirect(properties.RedirectUri); response.Redirect(properties.RedirectUri);
@ -799,10 +801,10 @@ namespace OpenIddict.Server.AspNetCore
} }
/// <summary> /// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. /// Contains the logic responsible of attaching an appropriate HTTP response code header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public class ProcessJsonResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext public class AttachHttpResponseCode<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -810,8 +812,8 @@ namespace OpenIddict.Server.AspNetCore
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>() .UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(ProcessPassthroughErrorResponse<TContext, IOpenIddictServerHandlerFilter<TContext>>.Descriptor.Order - 1_000) .SetOrder(AttachCacheControlHeader<TContext>.Descriptor.Order - 1_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -821,7 +823,7 @@ namespace OpenIddict.Server.AspNetCore
/// <returns> /// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation. /// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public async ValueTask HandleAsync([NotNull] TContext context) public ValueTask HandleAsync([NotNull] TContext context)
{ {
if (context == null) if (context == null)
{ {
@ -835,67 +837,303 @@ namespace OpenIddict.Server.AspNetCore
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // 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. // this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest(); var response = context.Transaction.GetHttpRequest()?.HttpContext.Response;
if (request == null) if (response == null)
{ {
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
} }
context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response); // When client authentication is made using basic authentication, the authorization server MUST return
// a 401 response with a valid WWW-Authenticate header containing the Basic scheme and a non-empty realm.
// A similar error MAY be returned even when basic authentication is not used and MUST also be returned
// when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme.
// To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors
// and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials
// were specified in the request form instead of the HTTP headers, as allowed by the specification.
response.StatusCode = context.Response.Error switch
{
null => 200, // Note: the default code may be replaced by another handler (e.g when doing redirects).
using var stream = new MemoryStream(); Errors.InvalidClient => 401,
await JsonSerializer.SerializeAsync(stream, context.Response, new JsonSerializerOptions Errors.InvalidToken => 401,
Errors.InsufficientAccess => 403,
Errors.InsufficientScope => 403,
_ => 400
};
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the appropriate HTTP response cache headers.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachCacheControlHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachCacheControlHeader<TContext>>()
.SetOrder(AttachWwwAuthenticateHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{ {
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, throw new ArgumentNullException(nameof(context));
WriteIndented = false }
});
// 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// Prevent the response from being cached.
response.Headers[HeaderNames.CacheControl] = "no-store";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching errors details to the WWW-Authenticate header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// When client authentication is made using basic authentication, the authorization server MUST return
// a 401 response with a valid WWW-Authenticate header containing the HTTP Basic authentication scheme.
// A similar error MAY be returned even when basic authentication is not used and MUST also be returned
// when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme.
// To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors
// and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials
// were specified in the request form instead of the HTTP headers, as allowed by the specification.
var scheme = context.Response.Error switch
{
Errors.InvalidClient => Schemes.Basic,
Errors.InvalidToken => Schemes.Bearer,
Errors.InsufficientAccess => Schemes.Bearer,
Errors.InsufficientScope => Schemes.Bearer,
_ => null
};
if (string.IsNullOrEmpty(scheme))
{
return default;
}
// Optimization: avoid allocating a StringBuilder if the
// WWW-Authenticate header doesn't contain any parameter.
if (string.IsNullOrEmpty(context.Response.Realm) &&
string.IsNullOrEmpty(context.Response.Error) &&
string.IsNullOrEmpty(context.Response.ErrorDescription) &&
string.IsNullOrEmpty(context.Response.ErrorUri) &&
string.IsNullOrEmpty(context.Response.Scope))
{
response.Headers.Append(HeaderNames.WWWAuthenticate, scheme);
return default;
}
var builder = new StringBuilder(scheme);
// Append the realm if one was specified.
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(' ');
builder.Append(Parameters.Realm);
builder.Append("=\"");
builder.Append(context.Response.Realm.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error if one was specified.
if (!string.IsNullOrEmpty(context.Response.Error)) if (!string.IsNullOrEmpty(context.Response.Error))
{ {
// When client authentication is made using basic authentication, the authorization server MUST return if (!string.IsNullOrEmpty(context.Response.Realm))
// a 401 response with a valid WWW-Authenticate header containing the Basic scheme and a non-empty realm.
// A similar error MAY be returned even when basic authentication is not used and MUST also be returned
// when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme.
// To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors
// and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials
// were specified in the request form instead of the HTTP headers, as allowed by the specification.
var scheme = context.Response.Error switch
{ {
Errors.InvalidClient => Schemes.Basic, builder.Append(',');
Errors.InvalidToken => Schemes.Bearer, }
_ => null
};
if (!string.IsNullOrEmpty(scheme)) builder.Append(' ');
builder.Append(Parameters.Error);
builder.Append("=\"");
builder.Append(context.Response.Error.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_description if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error))
{ {
if (context.Issuer == null) builder.Append(',');
{ }
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
request.HttpContext.Response.StatusCode = 401; builder.Append(' ');
builder.Append(Parameters.ErrorDescription);
builder.Append("=\"");
builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\""));
builder.Append('"');
}
request.HttpContext.Response.Headers[HeaderNames.WWWAuthenticate] = new StringBuilder() // Append the error_uri if one was specified.
.Append(scheme) if (!string.IsNullOrEmpty(context.Response.ErrorUri))
.Append(' ') {
.Append(Parameters.Realm) if (!string.IsNullOrEmpty(context.Response.Realm) ||
.Append("=\"") !string.IsNullOrEmpty(context.Response.Error) ||
.Append(context.Issuer.AbsoluteUri) !string.IsNullOrEmpty(context.Response.ErrorDescription))
.Append('"') {
.ToString(); builder.Append(',');
} }
else builder.Append(' ');
builder.Append(Parameters.ErrorUri);
builder.Append("=\"");
builder.Append(context.Response.ErrorUri.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the scope if one was specified.
if (!string.IsNullOrEmpty(context.Response.Scope))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{ {
request.HttpContext.Response.StatusCode = 400; builder.Append(',');
} }
builder.Append(' ');
builder.Append(Parameters.Scope);
builder.Append("=\"");
builder.Append(context.Response.Scope.Replace("\"", "\\\""));
builder.Append('"');
} }
request.HttpContext.Response.ContentLength = stream.Length; response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
request.HttpContext.Response.ContentType = "application/json;charset=UTF-8";
return default;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ProcessJsonResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(ProcessPassthroughErrorResponse<TContext, IOpenIddictServerHandlerFilter<TContext>>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response);
using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, context.Response, new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = false
});
response.ContentLength = stream.Length;
response.ContentType = "application/json;charset=UTF-8";
stream.Seek(offset: 0, loc: SeekOrigin.Begin); stream.Seek(offset: 0, loc: SeekOrigin.Begin);
await stream.CopyToAsync(request.HttpContext.Response.Body, 4096, request.HttpContext.RequestAborted); await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
context.HandleRequest(); context.HandleRequest();
} }
@ -949,9 +1187,6 @@ namespace OpenIddict.Server.AspNetCore
return default; return default;
} }
// Apply a 400 status code by default.
response.StatusCode = 400;
context.SkipRequest(); context.SkipRequest();
return default; return default;
@ -1017,9 +1252,6 @@ namespace OpenIddict.Server.AspNetCore
return default; return default;
} }
// Replace the default status code to return a 400 response.
response.StatusCode = 400;
// Mark the request as fully handled to prevent the other OpenIddict server handlers // Mark the request as fully handled to prevent the other OpenIddict server handlers
// from displaying the default error page and to allow the status code pages middleware // from displaying the default error page and to allow the status code pages middleware
// to rewrite the response using the logic defined by the developer when registering it. // to rewrite the response using the logic defined by the developer when registering it.
@ -1076,40 +1308,32 @@ namespace OpenIddict.Server.AspNetCore
// Don't return the state originally sent by the client application. // Don't return the state originally sent by the client application.
context.Response.State = null; context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.Logger.LogInformation("The authorization response was successfully returned " + context.Logger.LogInformation("The authorization response was successfully returned " +
"as a plain-text document: {Response}.", context.Response); "as a plain-text document: {Response}.", context.Response);
using (var buffer = new MemoryStream()) using var buffer = new MemoryStream();
using (var writer = new StreamWriter(buffer)) using var writer = new StreamWriter(buffer);
foreach (var parameter in context.Response.GetParameters())
{ {
foreach (var parameter in context.Response.GetParameters()) // Ignore null or empty parameters, including JSON
// objects that can't be represented as strings.
var value = (string) parameter.Value;
if (string.IsNullOrEmpty(value))
{ {
// Ignore null or empty parameters, including JSON continue;
// objects that can't be represented as strings.
var value = (string) parameter.Value;
if (string.IsNullOrEmpty(value))
{
continue;
}
writer.WriteLine("{0}:{1}", parameter.Key, value);
} }
writer.Flush(); writer.WriteLine("{0}:{1}", parameter.Key, value);
}
response.ContentLength = buffer.Length; writer.Flush();
response.ContentType = "text/plain;charset=UTF-8";
response.Headers[HeaderNames.CacheControl] = "no-cache"; response.ContentLength = buffer.Length;
response.Headers[HeaderNames.Pragma] = "no-cache"; response.ContentType = "text/plain;charset=UTF-8";
response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin); buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
}
context.HandleRequest(); context.HandleRequest();
} }
@ -1146,14 +1370,6 @@ namespace OpenIddict.Server.AspNetCore
throw new ArgumentNullException(nameof(context)); 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
context.Logger.LogInformation("The response was successfully returned as an empty 200 response."); context.Logger.LogInformation("The response was successfully returned as an empty 200 response.");
context.HandleRequest(); context.HandleRequest();

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -28,6 +28,8 @@ namespace OpenIddict.Server.Owin
public const string Error = ".error"; public const string Error = ".error";
public const string ErrorDescription = ".error_description"; public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri"; public const string ErrorUri = ".error_uri";
public const string Realm = ".realm";
public const string Scope = ".scope";
} }
} }
} }

4
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -112,6 +112,10 @@ namespace OpenIddict.Server.Owin
{ {
context = new ProcessAuthenticationContext(transaction); context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context); await _provider.DispatchAsync(context);
// 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.
transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName, context);
} }
if (context.IsRequestHandled || context.IsRequestSkipped) if (context.IsRequestHandled || context.IsRequestSkipped)

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -52,6 +52,8 @@ namespace OpenIddict.Server.Owin
* Authorization response processing: * Authorization response processing:
*/ */
RemoveCachedRequest.Descriptor, RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyAuthorizationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyAuthorizationResponseContext>.Descriptor,
ProcessFormPostResponse.Descriptor, ProcessFormPostResponse.Descriptor,
ProcessQueryResponse.Descriptor, ProcessQueryResponse.Descriptor,
ProcessFragmentResponse.Descriptor, ProcessFragmentResponse.Descriptor,

5
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs

@ -23,6 +23,9 @@ namespace OpenIddict.Server.Owin
/* /*
* Device response processing: * Device response processing:
*/ */
AttachHttpResponseCode<ApplyDeviceResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyDeviceResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyDeviceResponseContext>.Descriptor,
ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor, ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor,
/* /*
@ -38,6 +41,8 @@ namespace OpenIddict.Server.Owin
/* /*
* Verification response processing: * Verification response processing:
*/ */
AttachHttpResponseCode<ApplyVerificationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyVerificationResponseContext>.Descriptor,
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor, ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor, ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyVerificationResponseContext>.Descriptor, ProcessLocalErrorResponse<ApplyVerificationResponseContext>.Descriptor,

4
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs

@ -22,6 +22,8 @@ namespace OpenIddict.Server.Owin
/* /*
* Configuration response processing: * Configuration response processing:
*/ */
AttachHttpResponseCode<ApplyConfigurationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyConfigurationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyConfigurationResponseContext>.Descriptor, ProcessJsonResponse<ApplyConfigurationResponseContext>.Descriptor,
/* /*
@ -32,6 +34,8 @@ namespace OpenIddict.Server.Owin
/* /*
* Cryptography response processing: * Cryptography response processing:
*/ */
AttachHttpResponseCode<ApplyCryptographyResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyCryptographyResponseContext>.Descriptor,
ProcessJsonResponse<ApplyCryptographyResponseContext>.Descriptor); ProcessJsonResponse<ApplyCryptographyResponseContext>.Descriptor);
} }
} }

3
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs

@ -29,6 +29,9 @@ namespace OpenIddict.Server.Owin
/* /*
* Token response processing: * Token response processing:
*/ */
AttachHttpResponseCode<ApplyTokenResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyTokenResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyTokenResponseContext>.Descriptor,
ProcessJsonResponse<ApplyTokenResponseContext>.Descriptor); ProcessJsonResponse<ApplyTokenResponseContext>.Descriptor);
} }
} }

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs

@ -23,6 +23,8 @@ namespace OpenIddict.Server.Owin
/* /*
* Introspection response processing: * Introspection response processing:
*/ */
AttachHttpResponseCode<ApplyIntrospectionResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyIntrospectionResponseContext>.Descriptor,
ProcessJsonResponse<ApplyIntrospectionResponseContext>.Descriptor); ProcessJsonResponse<ApplyIntrospectionResponseContext>.Descriptor);
} }
} }

3
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs

@ -23,6 +23,9 @@ namespace OpenIddict.Server.Owin
/* /*
* Revocation response processing: * Revocation response processing:
*/ */
AttachHttpResponseCode<ApplyRevocationResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyRevocationResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyRevocationResponseContext>.Descriptor,
ProcessJsonResponse<ApplyRevocationResponseContext>.Descriptor); ProcessJsonResponse<ApplyRevocationResponseContext>.Descriptor);
} }
} }

4
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -50,8 +50,10 @@ namespace OpenIddict.Server.Owin
* Logout response processing: * Logout response processing:
*/ */
RemoveCachedRequest.Descriptor, RemoveCachedRequest.Descriptor,
AttachHttpResponseCode<ApplyLogoutResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyLogoutResponseContext>.Descriptor,
ProcessQueryResponse.Descriptor, ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor, ProcessHostRedirectionResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyLogoutResponseContext, RequireLogoutEndpointPassthroughEnabled>.Descriptor, ProcessPassthroughErrorResponse<ApplyLogoutResponseContext, RequireLogoutEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyLogoutResponseContext>.Descriptor, ProcessLocalErrorResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessEmptyResponse<ApplyLogoutResponseContext>.Descriptor); ProcessEmptyResponse<ApplyLogoutResponseContext>.Descriptor);

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs

@ -29,6 +29,8 @@ namespace OpenIddict.Server.Owin
/* /*
* Userinfo response processing: * Userinfo response processing:
*/ */
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyUserinfoResponseContext>.Descriptor,
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor); ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
} }
} }

347
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -314,12 +314,14 @@ namespace OpenIddict.Server.Owin
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) && var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
property is AuthenticationProperties properties) if (properties != null)
{ {
context.Response.Error = GetProperty(properties, Properties.Error); context.Response.Error = GetProperty(properties, Properties.Error);
context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription); context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription);
context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri); context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri);
context.Response.Realm = GetProperty(properties, Properties.Realm);
context.Response.Scope = GetProperty(properties, Properties.Scope);
} }
return default; return default;
@ -788,8 +790,8 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("The OWIN request cannot be resolved."); throw new InvalidOperationException("The OWIN request cannot be resolved.");
} }
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) && var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri)) if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri))
{ {
response.Redirect(properties.RedirectUri); response.Redirect(properties.RedirectUri);
@ -801,6 +803,283 @@ namespace OpenIddict.Server.Owin
} }
} }
/// <summary>
/// Contains the logic responsible of attaching an appropriate HTTP response code header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachHttpResponseCode<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(AttachCacheControlHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
// When client authentication is made using basic authentication, the authorization server MUST return
// a 401 response with a valid WWW-Authenticate header containing the Basic scheme and a non-empty realm.
// A similar error MAY be returned even when basic authentication is not used and MUST also be returned
// when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme.
// To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors
// and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials
// were specified in the request form instead of the HTTP headers, as allowed by the specification.
response.StatusCode = context.Response.Error switch
{
null => 200, // Note: the default code may be replaced by another handler (e.g when doing redirects).
Errors.InvalidClient => 401,
Errors.InvalidToken => 401,
Errors.InsufficientAccess => 403,
Errors.InsufficientScope => 403,
_ => 400
};
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the appropriate HTTP response cache headers.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachCacheControlHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachCacheControlHeader<TContext>>()
.SetOrder(AttachWwwAuthenticateHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// Prevent the response from being cached.
response.Headers["Cache-Control"] = "no-store";
response.Headers["Pragma"] = "no-cache";
response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT";
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching errors details to the WWW-Authenticate header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
// When client authentication is made using basic authentication, the authorization server MUST return
// a 401 response with a valid WWW-Authenticate header containing the HTTP Basic authentication scheme.
// A similar error MAY be returned even when basic authentication is not used and MUST also be returned
// when an invalid token is received by the userinfo endpoint using the Bearer authentication scheme.
// To simplify the logic, a 401 response with the Bearer scheme is returned for invalid_token errors
// and a 401 response with the Basic scheme is returned for invalid_client, even if the credentials
// were specified in the request form instead of the HTTP headers, as allowed by the specification.
var scheme = context.Response.Error switch
{
Errors.InvalidClient => Schemes.Basic,
Errors.InvalidToken => Schemes.Bearer,
Errors.InsufficientAccess => Schemes.Bearer,
Errors.InsufficientScope => Schemes.Bearer,
_ => null
};
if (string.IsNullOrEmpty(scheme))
{
return default;
}
// Optimization: avoid allocating a StringBuilder if the
// WWW-Authenticate header doesn't contain any parameter.
if (string.IsNullOrEmpty(context.Response.Realm) &&
string.IsNullOrEmpty(context.Response.Error) &&
string.IsNullOrEmpty(context.Response.ErrorDescription) &&
string.IsNullOrEmpty(context.Response.ErrorUri) &&
string.IsNullOrEmpty(context.Response.Scope))
{
response.Headers.Append("WWW-Authenticate", scheme);
return default;
}
var builder = new StringBuilder(scheme);
// Append the realm if one was specified.
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(' ');
builder.Append(Parameters.Realm);
builder.Append("=\"");
builder.Append(context.Response.Realm.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error if one was specified.
if (!string.IsNullOrEmpty(context.Response.Error))
{
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Error);
builder.Append("=\"");
builder.Append(context.Response.Error.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_description if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorDescription);
builder.Append("=\"");
builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_uri if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorUri))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorUri);
builder.Append("=\"");
builder.Append(context.Response.ErrorUri.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the scope if one was specified.
if (!string.IsNullOrEmpty(context.Response.Scope))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Scope);
builder.Append("=\"");
builder.Append(context.Response.Scope.Replace("\"", "\\\""));
builder.Append('"');
}
response.Headers.Append("WWW-Authenticate", builder.ToString());
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. /// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
@ -853,15 +1132,6 @@ namespace OpenIddict.Server.Owin
WriteIndented = false WriteIndented = false
}); });
if (!string.IsNullOrEmpty(context.Response.Error))
{
// Note: when using basic authentication, returning an invalid_client error MUST result in
// an unauthorized response but returning a 401 status code would invoke the previously
// registered authentication middleware and potentially replace it by a 302 response.
// To work around this OWIN/Katana limitation, a 400 response code is always returned.
response.StatusCode = 400;
}
response.ContentLength = stream.Length; response.ContentLength = stream.Length;
response.ContentType = "application/json;charset=UTF-8"; response.ContentType = "application/json;charset=UTF-8";
@ -923,9 +1193,6 @@ namespace OpenIddict.Server.Owin
// Don't return the state originally sent by the client application. // Don't return the state originally sent by the client application.
context.Response.State = null; context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.SkipRequest(); context.SkipRequest();
return default; return default;
@ -979,40 +1246,32 @@ namespace OpenIddict.Server.Owin
// Don't return the state originally sent by the client application. // Don't return the state originally sent by the client application.
context.Response.State = null; context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.Logger.LogInformation("The authorization response was successfully returned " + context.Logger.LogInformation("The authorization response was successfully returned " +
"as a plain-text document: {Response}.", context.Response); "as a plain-text document: {Response}.", context.Response);
using (var buffer = new MemoryStream()) using var buffer = new MemoryStream();
using (var writer = new StreamWriter(buffer)) using var writer = new StreamWriter(buffer);
foreach (var parameter in context.Response.GetParameters())
{ {
foreach (var parameter in context.Response.GetParameters()) // Ignore null or empty parameters, including JSON
// objects that can't be represented as strings.
var value = (string) parameter.Value;
if (string.IsNullOrEmpty(value))
{ {
// Ignore null or empty parameters, including JSON continue;
// objects that can't be represented as strings.
var value = (string) parameter.Value;
if (string.IsNullOrEmpty(value))
{
continue;
}
writer.WriteLine("{0}:{1}", parameter.Key, value);
} }
writer.Flush(); writer.WriteLine("{0}:{1}", parameter.Key, value);
}
response.ContentLength = buffer.Length; writer.Flush();
response.ContentType = "text/plain;charset=UTF-8";
response.Headers["Cache-Control"] = "no-cache"; response.ContentLength = buffer.Length;
response.Headers["Pragma"] = "no-cache"; response.ContentType = "text/plain;charset=UTF-8";
response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin); buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled);
}
context.HandleRequest(); context.HandleRequest();
} }
@ -1049,14 +1308,6 @@ namespace OpenIddict.Server.Owin
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
context.Logger.LogInformation("The response was successfully returned as an empty 200 response."); context.Logger.LogInformation("The response was successfully returned as an empty 200 response.");
context.HandleRequest(); context.HandleRequest();

4
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -408,7 +408,7 @@ namespace OpenIddict.Server
context.Logger.LogError("The device request was rejected because the mandatory 'client_id' was missing."); context.Logger.LogError("The device request was rejected because the mandatory 'client_id' was missing.");
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing."); description: "The mandatory 'client_id' parameter is missing.");
return default; return default;
@ -610,7 +610,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId); "was not allowed to send a client secret.", context.ClientId);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application."); description: "The 'client_secret' parameter is not valid for this client application.");
return; return;

4
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -487,7 +487,7 @@ namespace OpenIddict.Server
context.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing."); context.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing.");
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing."); description: "The mandatory 'client_id' parameter is missing.");
return default; return default;
@ -931,7 +931,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId); "was not allowed to send a client secret.", context.ClientId);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application."); description: "The 'client_secret' parameter is not valid for this client application.");
return; return;

4
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -459,7 +459,7 @@ namespace OpenIddict.Server
context.Logger.LogError("The introspection request was rejected because the mandatory 'client_id' was missing."); context.Logger.LogError("The introspection request was rejected because the mandatory 'client_id' was missing.");
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing."); description: "The mandatory 'client_id' parameter is missing.");
return default; return default;
@ -590,7 +590,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId); "was not allowed to send a client secret.", context.ClientId);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application."); description: "The 'client_secret' parameter is not valid for this client application.");
return; return;

4
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -405,7 +405,7 @@ namespace OpenIddict.Server
context.Logger.LogError("The revocation request was rejected because the mandatory 'client_id' was missing."); context.Logger.LogError("The revocation request was rejected because the mandatory 'client_id' was missing.");
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing."); description: "The mandatory 'client_id' parameter is missing.");
return default; return default;
@ -536,7 +536,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId); "was not allowed to send a client secret.", context.ClientId);
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application."); description: "The 'client_secret' parameter is not valid for this client application.");
return; return;

44
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -1055,31 +1055,35 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (string.IsNullOrEmpty(context.Response.Error)) // If error details were explicitly set by the application, don't override them.
if (!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{ {
context.Response.Error = context.EndpointType switch return default;
{
OpenIddictServerEndpointType.Authorization => Errors.AccessDenied,
OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
OpenIddictServerEndpointType.Userinfo => Errors.InvalidToken,
OpenIddictServerEndpointType.Verification => Errors.AccessDenied,
_ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.")
};
} }
if (string.IsNullOrEmpty(context.Response.ErrorDescription)) context.Response.Error = context.EndpointType switch
{ {
context.Response.ErrorDescription = context.EndpointType switch OpenIddictServerEndpointType.Authorization => Errors.AccessDenied,
{ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", OpenIddictServerEndpointType.Userinfo => Errors.InsufficientAccess,
OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", OpenIddictServerEndpointType.Verification => Errors.AccessDenied,
OpenIddictServerEndpointType.Userinfo => "The access token is not valid or cannot be used to retrieve user information.",
OpenIddictServerEndpointType.Verification => "The authorization was denied by the resource owner.",
_ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.")
}; };
}
context.Response.ErrorDescription = context.EndpointType switch
{
OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.",
OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.",
OpenIddictServerEndpointType.Userinfo => "The user information access demand was rejected by the authorization server.",
OpenIddictServerEndpointType.Verification => "The authorization was denied by the resource owner.",
_ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.")
};
context.Response.Realm = context.Options.Realm;
return default; return default;
} }

6
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -292,6 +292,12 @@ namespace OpenIddict.Server
/// </summary> /// </summary>
public bool IgnoreScopePermissions { get; set; } public bool IgnoreScopePermissions { get; set; }
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
/// <summary> /// <summary>
/// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application. /// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application.
/// </summary> /// </summary>

2
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs

@ -22,6 +22,8 @@ namespace OpenIddict.Validation.AspNetCore
public const string Error = ".error"; public const string Error = ".error";
public const string ErrorDescription = ".error_description"; public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri"; public const string ErrorUri = ".error_uri";
public const string Realm = ".realm";
public const string Scope = ".scope";
} }
} }
} }

71
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,7 +17,6 @@ using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties;
namespace OpenIddict.Validation.AspNetCore namespace OpenIddict.Validation.AspNetCore
{ {
@ -111,52 +111,35 @@ namespace OpenIddict.Validation.AspNetCore
throw new InvalidOperationException("An identity cannot be extracted from this request."); throw new InvalidOperationException("An identity cannot be extracted from this request.");
} }
var context = new ProcessAuthenticationContext(transaction); // Note: in many cases, the authentication token was already validated by the time this action is called
await _provider.DispatchAsync(context); // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it,
// the authentication context is resolved from the transaction. If it's not available, a new one is created.
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName);
if (context == null)
{
context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
// 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.
transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName, context);
}
if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped) if (context.IsRequestHandled || context.IsRequestSkipped)
{ {
return AuthenticateResult.NoResult(); return AuthenticateResult.NoResult();
} }
else if (context.IsRejected) else if (context.IsRejected)
{ {
var builder = new StringBuilder(); var properties = new AuthenticationProperties(new Dictionary<string, string>
if (!string.IsNullOrEmpty(context.Error))
{ {
builder.AppendLine("An error occurred while authenticating the current request:"); [OpenIddictValidationAspNetCoreConstants.Properties.Error] = context.Error,
builder.AppendFormat("Error code: ", context.Error); [OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription,
[OpenIddictValidationAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri
if (!string.IsNullOrEmpty(context.ErrorDescription))
{
builder.AppendLine();
builder.AppendFormat("Error description: ", context.ErrorDescription);
}
if (!string.IsNullOrEmpty(context.ErrorUri))
{
builder.AppendLine();
builder.AppendFormat("Error URI: ", context.ErrorUri);
}
}
else
{
builder.Append("An unknown error occurred while authenticating the current request.");
}
return AuthenticateResult.Fail(new Exception(builder.ToString())
{
// Note: the error details are stored as additional exception properties,
// which is similar to what other ASP.NET Core security handlers do.
Data =
{
[Parameters.Error] = context.Error,
[Parameters.ErrorDescription] = context.ErrorDescription,
[Parameters.ErrorUri] = context.ErrorUri
}
}); });
return AuthenticateResult.Fail("An error occurred while authenticating the current request.", properties);
} }
return AuthenticateResult.Success(new AuthenticationTicket( return AuthenticateResult.Success(new AuthenticationTicket(
@ -172,14 +155,11 @@ namespace OpenIddict.Validation.AspNetCore
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
} }
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction) var context = new ProcessChallengeContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse()
{
Error = GetProperty(properties, Properties.Error),
ErrorDescription = GetProperty(properties, Properties.ErrorDescription),
ErrorUri = GetProperty(properties, Properties.ErrorUri)
}
}; };
await _provider.DispatchAsync(context); await _provider.DispatchAsync(context);
@ -214,9 +194,6 @@ namespace OpenIddict.Validation.AspNetCore
.Append("was not registered or was explicitly removed from the handlers list.") .Append("was not registered or was explicitly removed from the handlers list.")
.ToString()); .ToString());
} }
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Items.TryGetValue(name, out string value) ? value : null;
} }
protected override Task HandleForbiddenAsync([CanBeNull] AuthenticationProperties properties) protected override Task HandleForbiddenAsync([CanBeNull] AuthenticationProperties properties)

349
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs

@ -14,6 +14,7 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -21,6 +22,7 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters; using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties;
namespace OpenIddict.Validation.AspNetCore namespace OpenIddict.Validation.AspNetCore
{ {
@ -35,10 +37,22 @@ namespace OpenIddict.Validation.AspNetCore
ExtractGetOrPostRequest.Descriptor, ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor, ExtractAccessToken.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor,
/* /*
* Response processing: * Response processing:
*/ */
AttachHttpResponseCode<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor, ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor); ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary> /// <summary>
@ -218,6 +232,310 @@ namespace OpenIddict.Validation.AspNetCore
} }
} }
/// <summary>
/// Contains the logic responsible of attaching the error details using the ASP.NET Core authentication properties.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachHostChallengeError : IOpenIddictValidationHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachHostChallengeError>()
.SetOrder(int.MinValue + 50_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessChallengeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
if (properties != null)
{
context.Response.Error = properties.GetString(Properties.Error);
context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription);
context.Response.ErrorUri = properties.GetString(Properties.ErrorUri);
context.Response.Realm = properties.GetString(Properties.Realm);
context.Response.Scope = properties.GetString(Properties.Scope);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching an appropriate HTTP response code header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachHttpResponseCode<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(AttachCacheControlHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// 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;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
response.StatusCode = context.Response.Error switch
{
null => 200,
Errors.InvalidToken => 401,
Errors.InsufficientAccess => 403,
Errors.InsufficientScope => 403,
_ => 400
};
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the appropriate HTTP response cache headers.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachCacheControlHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachCacheControlHeader<TContext>>()
.SetOrder(AttachWwwAuthenticateHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// Prevent the response from being cached.
response.Headers[HeaderNames.CacheControl] = "no-store";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching errors details to the WWW-Authenticate header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// 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;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
var scheme = context.Response.Error switch
{
Errors.InvalidToken => Schemes.Bearer,
Errors.InsufficientAccess => Schemes.Bearer,
Errors.InsufficientScope => Schemes.Bearer,
_ => null
};
if (string.IsNullOrEmpty(scheme))
{
return default;
}
// Optimization: avoid allocating a StringBuilder if the
// WWW-Authenticate header doesn't contain any parameter.
if (string.IsNullOrEmpty(context.Response.Realm) &&
string.IsNullOrEmpty(context.Response.Error) &&
string.IsNullOrEmpty(context.Response.ErrorDescription) &&
string.IsNullOrEmpty(context.Response.ErrorUri) &&
string.IsNullOrEmpty(context.Response.Scope))
{
response.Headers.Append(HeaderNames.WWWAuthenticate, scheme);
return default;
}
var builder = new StringBuilder(scheme);
// Append the realm if one was specified.
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(' ');
builder.Append(Parameters.Realm);
builder.Append("=\"");
builder.Append(context.Response.Realm.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error if one was specified.
if (!string.IsNullOrEmpty(context.Response.Error))
{
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Error);
builder.Append("=\"");
builder.Append(context.Response.Error.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_description if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorDescription);
builder.Append("=\"");
builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_uri if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorUri))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorUri);
builder.Append("=\"");
builder.Append(context.Response.ErrorUri.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the scope if one was specified.
if (!string.IsNullOrEmpty(context.Response.Scope))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Scope);
builder.Append("=\"");
builder.Append(context.Response.Scope.Replace("\"", "\\\""));
builder.Append('"');
}
response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. /// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
@ -231,7 +549,7 @@ namespace OpenIddict.Validation.AspNetCore
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>() .UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000) .SetOrder(int.MaxValue - 100_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -255,8 +573,8 @@ namespace OpenIddict.Validation.AspNetCore
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, // 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. // this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest(); var response = context.Transaction.GetHttpRequest()?.HttpContext.Response;
if (request == null) if (response == null)
{ {
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
} }
@ -270,30 +588,11 @@ namespace OpenIddict.Validation.AspNetCore
WriteIndented = false WriteIndented = false
}); });
if (!string.IsNullOrEmpty(context.Response.Error)) response.ContentLength = stream.Length;
{ response.ContentType = "application/json;charset=UTF-8";
if (context.Issuer == null)
{
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
request.HttpContext.Response.StatusCode = 401;
request.HttpContext.Response.Headers[HeaderNames.WWWAuthenticate] = new StringBuilder()
.Append(Schemes.Bearer)
.Append(' ')
.Append(Parameters.Realm)
.Append("=\"")
.Append(context.Issuer.AbsoluteUri)
.Append('"')
.ToString();
}
request.HttpContext.Response.ContentLength = stream.Length;
request.HttpContext.Response.ContentType = "application/json;charset=UTF-8";
stream.Seek(offset: 0, loc: SeekOrigin.Begin); stream.Seek(offset: 0, loc: SeekOrigin.Begin);
await stream.CopyToAsync(request.HttpContext.Response.Body, 4096, request.HttpContext.RequestAborted); await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
context.HandleRequest(); context.HandleRequest();
} }

2
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs

@ -22,6 +22,8 @@ namespace OpenIddict.Validation.Owin
public const string Error = ".error"; public const string Error = ".error";
public const string ErrorDescription = ".error_description"; public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri"; public const string ErrorUri = ".error_uri";
public const string Realm = ".realm";
public const string Scope = ".scope";
} }
} }
} }

44
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -15,7 +16,6 @@ using Microsoft.Owin.Security.Infrastructure;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties;
namespace OpenIddict.Validation.Owin namespace OpenIddict.Validation.Owin
{ {
@ -104,25 +104,35 @@ namespace OpenIddict.Validation.Owin
throw new InvalidOperationException("An identity cannot be extracted from this request."); throw new InvalidOperationException("An identity cannot be extracted from this request.");
} }
var context = new ProcessAuthenticationContext(transaction); // Note: in many cases, the authentication token was already validated by the time this action is called
await _provider.DispatchAsync(context); // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it,
// the authentication context is resolved from the transaction. If it's not available, a new one is created.
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName);
if (context == null)
{
context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped) // 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.
transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName, context);
}
if (context.IsRequestHandled || context.IsRequestSkipped)
{ {
return null; return null;
} }
else if (context.IsRejected) else if (context.IsRejected)
{ {
return new AuthenticationTicket(identity: null, new AuthenticationProperties var properties = new AuthenticationProperties(new Dictionary<string, string>
{ {
Dictionary = [OpenIddictValidationOwinConstants.Properties.Error] = context.Error,
{ [OpenIddictValidationOwinConstants.Properties.ErrorDescription] = context.ErrorDescription,
[Parameters.Error] = context.Error, [OpenIddictValidationOwinConstants.Properties.ErrorUri] = context.ErrorUri
[Parameters.ErrorDescription] = context.ErrorDescription,
[Parameters.ErrorUri] = context.ErrorUri
}
}); });
return new AuthenticationTicket(null, properties);
} }
return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, new AuthenticationProperties()); return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, new AuthenticationProperties());
@ -151,14 +161,11 @@ namespace OpenIddict.Validation.Owin
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
} }
transaction.Properties[typeof(AuthenticationProperties).FullName] = challenge.Properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction) var context = new ProcessChallengeContext(transaction)
{ {
Response = new OpenIddictResponse Response = new OpenIddictResponse()
{
Error = GetProperty(challenge.Properties, Properties.Error),
ErrorDescription = GetProperty(challenge.Properties, Properties.ErrorDescription),
ErrorUri = GetProperty(challenge.Properties, Properties.ErrorUri)
}
}; };
await _provider.DispatchAsync(context); await _provider.DispatchAsync(context);
@ -193,9 +200,6 @@ namespace OpenIddict.Validation.Owin
.Append("was not registered or was explicitly removed from the handlers list.") .Append("was not registered or was explicitly removed from the handlers list.")
.ToString()); .ToString());
} }
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Dictionary.TryGetValue(name, out string value) ? value : null;
} }
} }
} }

357
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs

@ -14,11 +14,13 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using Owin; using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters; using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters;
using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties;
namespace OpenIddict.Validation.Owin namespace OpenIddict.Validation.Owin
{ {
@ -33,10 +35,22 @@ namespace OpenIddict.Validation.Owin
ExtractGetOrPostRequest.Descriptor, ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor, ExtractAccessToken.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor,
/* /*
* Response processing: * Response processing:
*/ */
AttachHttpResponseCode<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor, ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor); ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary> /// <summary>
@ -217,6 +231,318 @@ namespace OpenIddict.Validation.Owin
} }
} }
/// <summary>
/// Contains the logic responsible of attaching the error details using the OWIN authentication properties.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachHostChallengeError : IOpenIddictValidationHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachHostChallengeError>()
.SetOrder(int.MinValue + 50_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessChallengeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
if (properties != null)
{
context.Response.Error = GetProperty(properties, Properties.Error);
context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription);
context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri);
context.Response.Realm = GetProperty(properties, Properties.Realm);
context.Response.Scope = GetProperty(properties, Properties.Scope);
}
return default;
static string GetProperty(AuthenticationProperties properties, string name)
=> properties.Dictionary.TryGetValue(name, out string value) ? value : null;
}
}
/// <summary>
/// Contains the logic responsible of attaching an appropriate HTTP response code header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachHttpResponseCode<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(AttachCacheControlHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
response.StatusCode = context.Response.Error switch
{
null => 200,
Errors.InvalidToken => 401,
Errors.InsufficientAccess => 403,
Errors.InsufficientScope => 403,
_ => 400
};
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the appropriate HTTP response cache headers.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachCacheControlHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachCacheControlHeader<TContext>>()
.SetOrder(AttachWwwAuthenticateHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
// Prevent the response from being cached.
response.Headers["Cache-Control"] = "no-store";
response.Headers["Pragma"] = "no-cache";
response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT";
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching errors details to the WWW-Authenticate header.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response == null)
{
throw new InvalidOperationException("This handler cannot be invoked without a response attached.");
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
if (string.IsNullOrEmpty(context.Response.Error))
{
return default;
}
var scheme = context.Response.Error switch
{
Errors.InvalidToken => Schemes.Bearer,
Errors.InsufficientAccess => Schemes.Bearer,
Errors.InsufficientScope => Schemes.Bearer,
_ => null
};
if (string.IsNullOrEmpty(scheme))
{
return default;
}
// Optimization: avoid allocating a StringBuilder if the
// WWW-Authenticate header doesn't contain any parameter.
if (string.IsNullOrEmpty(context.Response.Realm) &&
string.IsNullOrEmpty(context.Response.Error) &&
string.IsNullOrEmpty(context.Response.ErrorDescription) &&
string.IsNullOrEmpty(context.Response.ErrorUri) &&
string.IsNullOrEmpty(context.Response.Scope))
{
response.Headers.Append("WWW-Authenticate", scheme);
return default;
}
var builder = new StringBuilder(scheme);
// Append the realm if one was specified.
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(' ');
builder.Append(Parameters.Realm);
builder.Append("=\"");
builder.Append(context.Response.Realm.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error if one was specified.
if (!string.IsNullOrEmpty(context.Response.Error))
{
if (!string.IsNullOrEmpty(context.Response.Realm))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Error);
builder.Append("=\"");
builder.Append(context.Response.Error.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_description if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorDescription);
builder.Append("=\"");
builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the error_uri if one was specified.
if (!string.IsNullOrEmpty(context.Response.ErrorUri))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.ErrorUri);
builder.Append("=\"");
builder.Append(context.Response.ErrorUri.Replace("\"", "\\\""));
builder.Append('"');
}
// Append the scope if one was specified.
if (!string.IsNullOrEmpty(context.Response.Scope))
{
if (!string.IsNullOrEmpty(context.Response.Realm) ||
!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{
builder.Append(',');
}
builder.Append(' ');
builder.Append(Parameters.Scope);
builder.Append("=\"");
builder.Append(context.Response.Scope.Replace("\"", "\\\""));
builder.Append('"');
}
response.Headers.Append("WWW-Authenticate", builder.ToString());
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON. /// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
@ -230,7 +556,7 @@ namespace OpenIddict.Validation.Owin
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>() .UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000) .SetOrder(int.MaxValue - 100_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -254,8 +580,8 @@ namespace OpenIddict.Validation.Owin
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved, // This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack. // this may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetOwinRequest(); var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (request == null) if (response == null)
{ {
throw new InvalidOperationException("The OWIN request cannot be resolved."); throw new InvalidOperationException("The OWIN request cannot be resolved.");
} }
@ -269,30 +595,11 @@ namespace OpenIddict.Validation.Owin
WriteIndented = false WriteIndented = false
}); });
if (!string.IsNullOrEmpty(context.Response.Error)) response.ContentLength = stream.Length;
{ response.ContentType = "application/json;charset=UTF-8";
if (context.Issuer == null)
{
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
request.Context.Response.StatusCode = 401;
request.Context.Response.Headers["WWW-Authenticate"] = new StringBuilder()
.Append(Schemes.Bearer)
.Append(' ')
.Append(Parameters.Realm)
.Append("=\"")
.Append(context.Issuer.AbsoluteUri)
.Append('"')
.ToString();
}
request.Context.Response.ContentLength = stream.Length;
request.Context.Response.ContentType = "application/json;charset=UTF-8";
stream.Seek(offset: 0, loc: SeekOrigin.Begin); stream.Seek(offset: 0, loc: SeekOrigin.Begin);
await stream.CopyToAsync(request.Context.Response.Body, 4096, request.CallCancelled); await stream.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled);
context.HandleRequest(); context.HandleRequest();
} }

34
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -5,7 +5,6 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
@ -76,7 +75,7 @@ namespace OpenIddict.Validation
context.Logger.LogError("The request was rejected because the access token was missing."); context.Logger.LogError("The request was rejected because the access token was missing.");
context.Reject( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidRequest,
description: "The access token is missing."); description: "The access token is missing.");
return default; return default;
@ -533,16 +532,39 @@ namespace OpenIddict.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (string.IsNullOrEmpty(context.Response.Error)) // If an error was explicitly set by the application, don't override it.
if (!string.IsNullOrEmpty(context.Response.Error) ||
!string.IsNullOrEmpty(context.Response.ErrorDescription) ||
!string.IsNullOrEmpty(context.Response.ErrorUri))
{
return default;
}
// Try to retrieve the authentication context from the validation transaction and use
// the error details returned during the authentication processing, if available.
// If no error is attached to the authentication context, this likely means that
// the request was rejected very early without even checking the access token or was
// rejected due to a lack of permission. In this case, return an insufficient_access error
// to inform the client that the user is not allowed to perform the requested action.
var notification = context.Transaction.GetProperty<ProcessAuthenticationContext>(
typeof(ProcessAuthenticationContext).FullName);
if (!string.IsNullOrEmpty(notification?.Error))
{ {
context.Response.Error = Errors.InvalidToken; context.Response.Error = notification.Error;
context.Response.ErrorDescription = notification.ErrorDescription;
context.Response.ErrorUri = notification.ErrorUri;
} }
if (string.IsNullOrEmpty(context.Response.ErrorDescription)) else
{ {
context.Response.ErrorDescription = "The access token is not valid."; context.Response.Error = Errors.InsufficientAccess;
context.Response.ErrorDescription = "The user represented by the token is not allowed to perform the requested action.";
} }
context.Response.Realm = context.Options.Realm;
return default; return default;
} }
} }

80
src/OpenIddict.Validation/OpenIddictValidationHelpers.cs

@ -0,0 +1,80 @@
/*
* 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;
using JetBrains.Annotations;
namespace OpenIddict.Validation
{
/// <summary>
/// Exposes extensions simplifying the integration with the OpenIddict validation services.
/// </summary>
public static class OpenIddictValidationHelpers
{
/// <summary>
/// Retrieves a property value from the validation transaction using the specified name.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="transaction">The validation transaction.</param>
/// <param name="name">The property name.</param>
/// <returns>The property value or <c>null</c> if it couldn't be found.</returns>
public static TProperty GetProperty<TProperty>(
[NotNull] this OpenIddictValidationTransaction transaction, [NotNull] string name) where TProperty : class
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(name));
}
if (transaction.Properties.TryGetValue(name, out var property) && property is TProperty result)
{
return result;
}
return null;
}
/// <summary>
/// Sets a property in the validation transaction using the specified name and value.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="transaction">The validation transaction.</param>
/// <param name="name">The property name.</param>
/// <param name="value">The property value.</param>
/// <returns>The validation transaction, so that calls can be easily chained.</returns>
public static OpenIddictValidationTransaction SetProperty<TProperty>(
[NotNull] this OpenIddictValidationTransaction transaction,
[NotNull] string name, [CanBeNull] TProperty value) where TProperty : class
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(name));
}
if (value == null)
{
transaction.Properties.Remove(name);
}
else
{
transaction.Properties[name] = value;
}
return transaction;
}
}
}

6
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -78,6 +78,12 @@ namespace OpenIddict.Validation
/// </summary> /// </summary>
public ISet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal); public ISet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
/// <summary> /// <summary>
/// Gets the token validation parameters used by the OpenIddict validation services. /// Gets the token validation parameters used by the OpenIddict validation services.
/// </summary> /// </summary>

7
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs

@ -71,6 +71,13 @@ namespace OpenIddict.Abstractions.Tests.Primitives
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4") /* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
}; };
yield return new object[]
{
/* property: */ nameof(OpenIddictResponse.Realm),
/* name: */ OpenIddictConstants.Parameters.Realm,
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[] yield return new object[]
{ {
/* property: */ nameof(OpenIddictResponse.RefreshToken), /* property: */ nameof(OpenIddictResponse.RefreshToken),

6
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -161,7 +161,7 @@ namespace OpenIddict.Server.FunctionalTests
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
} }
@ -1156,7 +1156,7 @@ namespace OpenIddict.Server.FunctionalTests
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
} }
@ -1258,7 +1258,7 @@ namespace OpenIddict.Server.FunctionalTests
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());

20
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs

@ -408,6 +408,26 @@ namespace OpenIddict.Server.FunctionalTests
Assert.Equal("The client application is not allowed to introspect the specified token.", response.ErrorDescription); Assert.Equal("The client application is not allowed to introspect the specified token.", response.ErrorDescription);
} }
[Fact]
public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired()
{
// Arrange
var client = CreateClient(builder =>
{
builder.Configure(options => options.AcceptAnonymousClients = false);
});
// Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{
Token = "2YotnFZFEjr1zCsicMWpAA"
});
// Assert
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
}
[Fact] [Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound()
{ {

4
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs

@ -364,7 +364,7 @@ namespace OpenIddict.Server.FunctionalTests
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
} }
@ -471,7 +471,7 @@ namespace OpenIddict.Server.FunctionalTests
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription);
Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()), Times.AtLeastOnce());

45
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -533,6 +533,51 @@ namespace OpenIddict.Server.FunctionalTests
Assert.Null(response.ErrorUri); Assert.Null(response.ErrorUri);
} }
[Fact]
public async Task ProcessChallenge_ReturnsDefaultErrorForUserinfoRequestsWhenNoneIsSpecified()
{
// Arrange
var client = CreateClient(options =>
{
options.EnableDegradedMode();
options.SetUserinfoEndpointUris("/challenge");
options.AddEventHandler<ProcessAuthenticationContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("SlAV32hkKG", context.Token);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.AddEventHandler<HandleUserinfoRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
});
// Act
var response = await client.PostAsync("/challenge", new OpenIddictRequest
{
AccessToken = "SlAV32hkKG"
});
// Assert
Assert.Equal(Errors.InsufficientAccess, response.Error);
Assert.Equal("The user information access demand was rejected by the authorization server.", response.ErrorDescription);
Assert.Null(response.ErrorUri);
}
[Fact] [Fact]
public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties() public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties()
{ {

Loading…
Cancel
Save