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
{
private readonly HttpClient _client;
private readonly IHttpClientFactory _httpClientFactory;
public HomeController(HttpClient client)
{
_client = client;
}
public HomeController(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
[HttpGet("~/")]
public ActionResult Index()
{
return View("Home");
}
public ActionResult Index() => View("Home");
[Authorize, HttpPost("~/")]
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.");
}
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);
var response = await _client.SendAsync(request, cancellationToken);
using var client = _httpClientFactory.CreateClient();
using var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
return View("Home", model: await response.Content.ReadAsStringAsync());

6
samples/Mvc.Client/Startup.cs

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
@ -47,6 +46,7 @@ namespace Mvc.Client
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.Scope.Add("demo_api");
options.SecurityTokenValidator = new JwtSecurityTokenHandler
{
@ -60,9 +60,9 @@ namespace Mvc.Client
options.AccessDeniedPath = "/";
});
services.AddMvc();
services.AddHttpClient();
services.AddSingleton<HttpClient>();
services.AddMvc();
}
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 OpenIddictAuthorizationManager<OpenIddictAuthorization> _authorizationManager;
private readonly OpenIddictScopeManager<OpenIddictScope> _scopeManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
OpenIddictAuthorizationManager<OpenIddictAuthorization> authorizationManager,
OpenIddictScopeManager<OpenIddictScope> scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
_userManager = userManager;
}
@ -61,25 +64,29 @@ namespace Mvc.Server
// Retrieve the user principal stored in the authentication cookie.
// 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 the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(Prompts.None))
{
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// 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)));
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
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 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))
{
if (request.HasPrompt(Prompts.None))
{
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
return Challenge(
authenticationSchemes: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// 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.");
// 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.
var authorizations = await _authorizationManager.FindAsync(
subject: User.FindFirst(Claims.Subject)?.Value,
subject: await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
@ -144,12 +157,14 @@ namespace Mvc.Server
// 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.
case ConsentTypes.External when !authorizations.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[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,
// 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.
// For that, simply restrict the list of scopes before calling SetScopes.
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
// for future authorization or token requests containing the same scopes.
@ -171,7 +186,7 @@ namespace Mvc.Server
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value,
subject : await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes()));
@ -190,12 +205,14 @@ namespace Mvc.Server
// if the client application specified prompt=none in the authorization request.
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required.",
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
// In every other case, render the consent form.
default:
@ -224,7 +241,7 @@ namespace Mvc.Server
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await _authorizationManager.FindAsync(
subject: User.FindFirst(Claims.Subject)?.Value,
subject: await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application),
status : Statuses.Valid,
type : AuthorizationTypes.Permanent,
@ -236,12 +253,14 @@ namespace Mvc.Server
switch (await _applicationManager.GetConsentTypeAsync(application))
{
case ConsentTypes.External when !authorizations.Any():
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
}
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.
// For that, simply restrict the list of scopes before calling SetScopes.
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
// for future authorization or token requests containing the same scopes.
@ -259,7 +278,7 @@ namespace Mvc.Server
{
authorization = await _authorizationManager.CreateAsync(
principal: principal,
subject : principal.FindFirst(Claims.Subject)?.Value,
subject : await _userManager.GetUserIdAsync(user),
client : await _applicationManager.GetIdAsync(application),
type : AuthorizationTypes.Permanent,
scopes : ImmutableArray.CreateRange(principal.GetScopes()));
@ -318,8 +337,8 @@ namespace Mvc.Server
// Redisplay the form when the user code is not valid.
return View(new VerifyViewModel
{
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
Error = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
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.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(result.Principal.GetScopes());
principal.SetResources("resource_server");
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims)
{
@ -361,25 +380,22 @@ namespace Mvc.Server
// Redisplay the form when the user code is not valid.
return View(new VerifyViewModel
{
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
Error = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties?.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
});
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/verify"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner.
public IActionResult VerifyDeny()
{
var properties = new AuthenticationProperties
public IActionResult VerifyDeny() => Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties()
{
// This property points to the address OpenIddict will automatically
// redirect the user to after rejecting the authorization demand.
RedirectUri = "/"
};
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
});
#endregion
#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).
await _signInManager.SignOutAsync();
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// to the post_logout_redirect_uri specified by the client application or to
// the RedirectUri specified in the authentication properties if none was set.
return SignOut(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties
{
RedirectUri = "/"
});
}
#endregion
@ -423,26 +440,26 @@ namespace Mvc.Server
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
// Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
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.
// For that, simply restrict the list of scopes before calling SetScopes.
principal.SetScopes(request.GetScopes());
principal.SetResources("resource_server");
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
foreach (var claim in principal.Claims)
{
@ -474,25 +491,25 @@ namespace Mvc.Server
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
});
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
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.Identity;
using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models;
using OpenIddict.Abstractions;
using OpenIddict.Validation.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Mvc.Server.Controllers
{
@ -13,14 +17,28 @@ namespace Mvc.Server.Controllers
private readonly UserManager<ApplicationUser> _userManager;
public ResourceController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
=> _userManager = userManager;
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("message")]
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);
if (user == null)
{

143
samples/Mvc.Server/Startup.cs

@ -11,6 +11,7 @@ using Mvc.Server.Services;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Mvc.Server
{
@ -46,9 +47,9 @@ namespace Mvc.Server
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
services.AddOpenIddict()
@ -79,10 +80,8 @@ namespace Mvc.Server
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
// Mark the "email", "profile", "roles" and "demo_api" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
@ -125,6 +124,8 @@ namespace Mvc.Server
.AddValidation(options =>
{
// 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");
// Import the configuration from the local OpenIddict server instance.
@ -179,71 +180,89 @@ namespace Mvc.Server
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
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",
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 =
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
},
Requirements =
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Explicit,
DisplayName = "MVC client application",
PostLogoutRedirectUris = { new Uri("http://localhost:53507/signout-callback-oidc") },
RedirectUris = { new Uri("http://localhost:53507/signin-oidc") },
Permissions =
{
Permissions.Endpoints.Authorization,
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
}
};
await manager.CreateAsync(descriptor);
ClientId = "postman",
ConsentType = ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris = { new Uri("urn:postman") },
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:
//
// * 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)
static async Task RegisterScopesAsync(IServiceProvider provider)
{
var descriptor = new OpenIddictApplicationDescriptor
var manager = provider.GetRequiredService<OpenIddictScopeManager<OpenIddictScope>>();
if (await manager.FindByNameAsync("demo_api") == null)
{
ClientId = "postman",
ConsentType = OpenIddictConstants.ConsentTypes.Systematic,
DisplayName = "Postman",
RedirectUris = { new Uri("urn:postman") },
Permissions =
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Device,
OpenIddictConstants.Permissions.Endpoints.Token,
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);
DisplayName = "Demo API access",
Name = "demo_api",
Resources = { "resource_server" }
});
}
}
}
}

2
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -148,6 +148,8 @@ namespace OpenIddict.Abstractions
public const string AuthorizationPending = "authorization_pending";
public const string ConsentRequired = "consent_required";
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 InvalidClient = "invalid_client";
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);
}
/// <summary>
/// Gets or sets the "realm" parameter.
/// </summary>
public string Realm
{
get => (string) GetParameter(OpenIddictConstants.Parameters.Realm);
set => SetParameter(OpenIddictConstants.Parameters.Realm, value);
}
/// <summary>
/// Gets or sets the "refresh_token" parameter.
/// </summary>

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

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Generic;
using System.Text;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
@ -70,10 +71,10 @@ namespace OpenIddict.Server.AspNetCore
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 (string.IsNullOrEmpty(scheme) || !options.SchemeMap.TryGetValue(scheme, out var builder))
if (string.IsNullOrEmpty(scheme) || !map.TryGetValue(scheme, out var builder))
{
return true;
}
@ -81,9 +82,12 @@ namespace OpenIddict.Server.AspNetCore
return builder.HandlerType != typeof(OpenIddictServerAspNetCoreHandler);
}
if (!TryValidate(options.DefaultAuthenticateScheme) || !TryValidate(options.DefaultChallengeScheme) ||
!TryValidate(options.DefaultForbidScheme) || !TryValidate(options.DefaultScheme) ||
!TryValidate(options.DefaultSignInScheme) || !TryValidate(options.DefaultSignOutScheme))
if (!TryValidate(options.SchemeMap, options.DefaultAuthenticateScheme) ||
!TryValidate(options.SchemeMap, options.DefaultChallengeScheme) ||
!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()
.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 ErrorDescription = ".error_description";
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);
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)
@ -138,7 +142,7 @@ namespace OpenIddict.Server.AspNetCore
[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(

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -29,6 +29,8 @@ namespace OpenIddict.Server.AspNetCore
/*
* Userinfo response processing:
*/
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<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));
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties)
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;
@ -785,8 +787,8 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri))
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
if (properties != null && !string.IsNullOrEmpty(properties.RedirectUri))
{
response.Redirect(properties.RedirectUri);
@ -799,10 +801,10 @@ namespace OpenIddict.Server.AspNetCore
}
/// <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.
/// </summary>
public class ProcessJsonResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
public class AttachHttpResponseCode<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -810,8 +812,8 @@ namespace OpenIddict.Server.AspNetCore
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(ProcessPassthroughErrorResponse<TContext, IOpenIddictServerHandlerFilter<TContext>>.Descriptor.Order - 1_000)
.UseSingletonHandler<AttachHttpResponseCode<TContext>>()
.SetOrder(AttachCacheControlHeader<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
@ -821,7 +823,7 @@ namespace OpenIddict.Server.AspNetCore
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] TContext context)
public ValueTask HandleAsync([NotNull] TContext context)
{
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 may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
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);
// 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();
await JsonSerializer.SerializeAsync(stream, context.Response, new JsonSerializerOptions
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 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,
WriteIndented = false
});
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> : 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))
{
// 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.
var scheme = context.Response.Error switch
if (!string.IsNullOrEmpty(context.Response.Realm))
{
Errors.InvalidClient => Schemes.Basic,
Errors.InvalidToken => Schemes.Bearer,
_ => null
};
builder.Append(',');
}
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)
{
throw new InvalidOperationException("The issuer address cannot be inferred from the current request.");
}
builder.Append(',');
}
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(scheme)
.Append(' ')
.Append(Parameters.Realm)
.Append("=\"")
.Append(context.Issuer.AbsoluteUri)
.Append('"')
.ToString();
// 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(',');
}
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;
request.HttpContext.Response.ContentType = "application/json;charset=UTF-8";
response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
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);
await stream.CopyToAsync(request.HttpContext.Response.Body, 4096, request.HttpContext.RequestAborted);
await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
context.HandleRequest();
}
@ -949,9 +1187,6 @@ namespace OpenIddict.Server.AspNetCore
return default;
}
// Apply a 400 status code by default.
response.StatusCode = 400;
context.SkipRequest();
return default;
@ -1017,9 +1252,6 @@ namespace OpenIddict.Server.AspNetCore
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
// 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.
@ -1076,40 +1308,32 @@ namespace OpenIddict.Server.AspNetCore
// Don't return the state originally sent by the client application.
context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.Logger.LogInformation("The authorization response was successfully returned " +
"as a plain-text document: {Response}.", context.Response);
using (var buffer = new MemoryStream())
using (var writer = new StreamWriter(buffer))
using var buffer = new MemoryStream();
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
// 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);
continue;
}
writer.Flush();
writer.WriteLine("{0}:{1}", parameter.Key, value);
}
response.ContentLength = buffer.Length;
response.ContentType = "text/plain;charset=UTF-8";
writer.Flush();
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT";
response.ContentLength = buffer.Length;
response.ContentType = "text/plain;charset=UTF-8";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
}
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted);
context.HandleRequest();
}
@ -1146,14 +1370,6 @@ namespace OpenIddict.Server.AspNetCore
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.HandleRequest();

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

@ -28,6 +28,8 @@ namespace OpenIddict.Server.Owin
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
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);
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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -29,6 +29,8 @@ namespace OpenIddict.Server.Owin
/*
* Userinfo response processing:
*/
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<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));
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties)
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;
@ -788,8 +790,8 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri))
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName);
if (properties != null && !string.IsNullOrEmpty(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>
/// 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.
@ -853,15 +1132,6 @@ namespace OpenIddict.Server.Owin
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.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.
context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.SkipRequest();
return default;
@ -979,40 +1246,32 @@ namespace OpenIddict.Server.Owin
// Don't return the state originally sent by the client application.
context.Response.State = null;
// Apply a 400 status code by default.
response.StatusCode = 400;
context.Logger.LogInformation("The authorization response was successfully returned " +
"as a plain-text document: {Response}.", context.Response);
using (var buffer = new MemoryStream())
using (var writer = new StreamWriter(buffer))
using var buffer = new MemoryStream();
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
// 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);
continue;
}
writer.Flush();
writer.WriteLine("{0}:{1}", parameter.Key, value);
}
response.ContentLength = buffer.Length;
response.ContentType = "text/plain;charset=UTF-8";
writer.Flush();
response.Headers["Cache-Control"] = "no-cache";
response.Headers["Pragma"] = "no-cache";
response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT";
response.ContentLength = buffer.Length;
response.ContentType = "text/plain;charset=UTF-8";
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled);
}
buffer.Seek(offset: 0, loc: SeekOrigin.Begin);
await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled);
context.HandleRequest();
}
@ -1049,14 +1308,6 @@ namespace OpenIddict.Server.Owin
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.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.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing.");
return default;
@ -610,7 +610,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application.");
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.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing.");
return default;
@ -931,7 +931,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application.");
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.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing.");
return default;
@ -590,7 +590,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application.");
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.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The mandatory 'client_id' parameter is missing.");
return default;
@ -536,7 +536,7 @@ namespace OpenIddict.Server
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
error: Errors.InvalidRequest,
error: Errors.InvalidClient,
description: "The 'client_secret' parameter is not valid for this client application.");
return;

44
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -1055,31 +1055,35 @@ namespace OpenIddict.Server
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
{
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.")
};
return default;
}
if (string.IsNullOrEmpty(context.Response.ErrorDescription))
context.Response.Error = context.EndpointType switch
{
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 access token is not valid or cannot be used to retrieve user information.",
OpenIddictServerEndpointType.Verification => "The authorization was denied by the resource owner.",
OpenIddictServerEndpointType.Authorization => Errors.AccessDenied,
OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
OpenIddictServerEndpointType.Userinfo => Errors.InsufficientAccess,
OpenIddictServerEndpointType.Verification => Errors.AccessDenied,
_ => 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;
}

6
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -292,6 +292,12 @@ namespace OpenIddict.Server
/// </summary>
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>
/// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application.
/// </summary>

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

@ -22,6 +22,8 @@ namespace OpenIddict.Validation.AspNetCore
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
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.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
@ -16,7 +17,6 @@ using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties;
namespace OpenIddict.Validation.AspNetCore
{
@ -111,52 +111,35 @@ namespace OpenIddict.Validation.AspNetCore
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
// Note: in many cases, the authentication token was already validated by the time this action is called
// (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();
}
else if (context.IsRejected)
{
var builder = new StringBuilder();
if (!string.IsNullOrEmpty(context.Error))
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
builder.AppendLine("An error occurred while authenticating the current request:");
builder.AppendFormat("Error code: ", context.Error);
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
}
[OpenIddictValidationAspNetCoreConstants.Properties.Error] = context.Error,
[OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription,
[OpenIddictValidationAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri
});
return AuthenticateResult.Fail("An error occurred while authenticating the current request.", properties);
}
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.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(properties, Properties.Error),
ErrorDescription = GetProperty(properties, Properties.ErrorDescription),
ErrorUri = GetProperty(properties, Properties.ErrorUri)
}
Response = new OpenIddictResponse()
};
await _provider.DispatchAsync(context);
@ -214,9 +194,6 @@ namespace OpenIddict.Validation.AspNetCore
.Append("was not registered or was explicitly removed from the handlers list.")
.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)

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

@ -14,6 +14,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@ -21,6 +22,7 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties;
namespace OpenIddict.Validation.AspNetCore
{
@ -35,10 +37,22 @@ namespace OpenIddict.Validation.AspNetCore
ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor,
/*
* Response processing:
*/
AttachHttpResponseCode<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <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>
/// 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.
@ -231,7 +549,7 @@ namespace OpenIddict.Validation.AspNetCore
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000)
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <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 may indicate that the request was incorrectly processed by another server stack.
var request = context.Transaction.GetHttpRequest();
if (request == null)
var response = context.Transaction.GetHttpRequest()?.HttpContext.Response;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
@ -270,30 +588,11 @@ namespace OpenIddict.Validation.AspNetCore
WriteIndented = false
});
if (!string.IsNullOrEmpty(context.Response.Error))
{
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";
response.ContentLength = stream.Length;
response.ContentType = "application/json;charset=UTF-8";
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();
}

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

@ -22,6 +22,8 @@ namespace OpenIddict.Validation.Owin
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
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.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
@ -15,7 +16,6 @@ using Microsoft.Owin.Security.Infrastructure;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties;
namespace OpenIddict.Validation.Owin
{
@ -104,25 +104,35 @@ namespace OpenIddict.Validation.Owin
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
// Note: in many cases, the authentication token was already validated by the time this action is called
// (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;
}
else if (context.IsRejected)
{
return new AuthenticationTicket(identity: null, new AuthenticationProperties
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
Dictionary =
{
[Parameters.Error] = context.Error,
[Parameters.ErrorDescription] = context.ErrorDescription,
[Parameters.ErrorUri] = context.ErrorUri
}
[OpenIddictValidationOwinConstants.Properties.Error] = context.Error,
[OpenIddictValidationOwinConstants.Properties.ErrorDescription] = context.ErrorDescription,
[OpenIddictValidationOwinConstants.Properties.ErrorUri] = context.ErrorUri
});
return new AuthenticationTicket(null, properties);
}
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.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = challenge.Properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(challenge.Properties, Properties.Error),
ErrorDescription = GetProperty(challenge.Properties, Properties.ErrorDescription),
ErrorUri = GetProperty(challenge.Properties, Properties.ErrorUri)
}
Response = new OpenIddictResponse()
};
await _provider.DispatchAsync(context);
@ -193,9 +200,6 @@ namespace OpenIddict.Validation.Owin
.Append("was not registered or was explicitly removed from the handlers list.")
.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 JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Owin.Security;
using OpenIddict.Abstractions;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters;
using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties;
namespace OpenIddict.Validation.Owin
{
@ -33,10 +35,22 @@ namespace OpenIddict.Validation.Owin
ExtractGetOrPostRequest.Descriptor,
ExtractAccessToken.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor,
/*
* Response processing:
*/
AttachHttpResponseCode<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <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>
/// 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.
@ -230,7 +556,7 @@ namespace OpenIddict.Validation.Owin
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessJsonResponse<TContext>>()
.SetOrder(int.MinValue + 100_000)
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
@ -254,8 +580,8 @@ namespace OpenIddict.Validation.Owin
// 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 request = context.Transaction.GetOwinRequest();
if (request == null)
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
@ -269,30 +595,11 @@ namespace OpenIddict.Validation.Owin
WriteIndented = false
});
if (!string.IsNullOrEmpty(context.Response.Error))
{
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";
response.ContentLength = stream.Length;
response.ContentType = "application/json;charset=UTF-8";
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();
}

34
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -5,7 +5,6 @@
*/
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
@ -76,7 +75,7 @@ namespace OpenIddict.Validation
context.Logger.LogError("The request was rejected because the access token was missing.");
context.Reject(
error: Errors.InvalidToken,
error: Errors.InvalidRequest,
description: "The access token is missing.");
return default;
@ -533,16 +532,39 @@ namespace OpenIddict.Validation
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;
}
}

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>
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>
/// Gets the token validation parameters used by the OpenIddict validation services.
/// </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")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictResponse.Realm),
/* name: */ OpenIddictConstants.Parameters.Realm,
/* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4")
};
yield return new object[]
{
/* property: */ nameof(OpenIddictResponse.RefreshToken),

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

@ -161,7 +161,7 @@ namespace OpenIddict.Server.FunctionalTests
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
}
@ -1156,7 +1156,7 @@ namespace OpenIddict.Server.FunctionalTests
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
}
@ -1258,7 +1258,7 @@ namespace OpenIddict.Server.FunctionalTests
});
// 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);
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);
}
[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]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound()
{

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

@ -364,7 +364,7 @@ namespace OpenIddict.Server.FunctionalTests
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription);
}
@ -471,7 +471,7 @@ namespace OpenIddict.Server.FunctionalTests
});
// 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);
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);
}
[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]
public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties()
{

Loading…
Cancel
Save