orchardcore-security
Skill for configuring security and authorization in Orchard Core. Covers permission definitions, authorization services, CORS, security headers, content security policies, and OpenID Connect.
Files2
View on GitHubSKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-security/SKILL.md
GitHubOrchard Core Security - Prompt Templates
Configure Security and Authorization
You are an Orchard Core expert. Generate security and authorization configurations for Orchard Core.
Guidelines
- Orchard Core authorizes with permissions granted to roles. Check them with
IAuthorizationService.AuthorizeAsync(User, permission[, resource]). - Declare permissions as static fields on a
*Permissionsclass (usually in anAbstractionsorCoreproject, so other modules can reference them without depending on your module) and return them from anIPermissionProvider. Permission.Descriptionis aLocalizationSourcein 4.0. Create it withLocalizationSource.Create("text", typeof(DeclaringType))orLocalizationSource.Create<DeclaringType>("text")so the PO-file context is preserved. String descriptions still compile but are context-free.- Mark dangerous permissions with
isSecurityCritical: true. - Content permissions (
ViewContent,EditContent,PublishContent,DeleteContent, and theirOwnvariants) live inOrchardCore.Contents.CommonPermissions; pass the content item as the resource so dynamic per-type permissions apply. OrchardCore.Securityadds security headers configured from Settings → Security → Security Headers (permissionManageSecurityHeadersSettings).- API endpoints authenticate with the shared
Apischeme (OrchardCoreConstants.AuthenticationSchemes.Api), which accepts bearer tokens (for example fromOrchardCore.OpenId.Validation) and never cookies. - Related skills:
orchardcore-cors,orchardcore-https,orchardcore-reverse-proxy,orchardcore-openid,orchardcore-openapi,orchardcore-rate-limits,orchardcore-antivirus,orchardcore-file-upload-security,orchardcore-localization-source,orchardcore-users-roles. - All recipe JSON must be wrapped in the root
{ "steps": [...] }format. - All C# classes must use the
sealedmodifier, except View Models.
Enabling Security Features
{
"steps": [
{
"name": "feature",
"enable": [
"OrchardCore.Security",
"OrchardCore.Https",
"OrchardCore.Cors",
"OrchardCore.ReverseProxy",
"OrchardCore.RateLimits"
]
}
]
}
Security Headers via Recipe
{
"steps": [
{
"name": "settings",
"SecuritySettings": {
"ContentTypeOptions": "nosniff",
"ReferrerPolicy": "strict-origin-when-cross-origin",
"ContentSecurityPolicy": {
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self' 'unsafe-inline'",
"frame-ancestors": "'self'"
},
"PermissionsPolicy": {
"camera": "()",
"microphone": "()",
"geolocation": "()"
}
}
}
]
}
frame-ancestors replaces the obsolete X-Frame-Options header. Admin scripts in 4.0 load as ES modules without jQuery; test a strict script-src against the admin before removing 'unsafe-inline' for the whole site.
Security Headers from Configuration
Call ConfigureSecuritySettings() to let configuration override the admin values. The settings page then shows that values come from configuration.
builder.Services
.AddOrchardCms()
.ConfigureSecuritySettings();
{
"OrchardCore": {
"Security": {
"ContentSecurityPolicy": {
"default-src": "'self'"
},
"PermissionsPolicy": {
"fullscreen": "self"
},
"ReferrerPolicy": "no-referrer"
}
}
}
Declaring Permissions
using OrchardCore;
using OrchardCore.Localization;
using OrchardCore.Security.Permissions;
namespace MyModule;
public static class InvoicePermissions
{
public static readonly Permission ManageInvoices = new(
"ManageInvoices",
LocalizationSource.Create("Manage invoices", typeof(InvoicePermissions)),
isSecurityCritical: true);
public static readonly Permission ViewInvoices = new(
"ViewInvoices",
LocalizationSource.Create("View invoices", typeof(InvoicePermissions)),
[ManageInvoices]);
}
public sealed class Permissions : IPermissionProvider
{
private readonly IEnumerable<Permission> _allPermissions =
[
InvoicePermissions.ManageInvoices,
InvoicePermissions.ViewInvoices,
];
public Task<IEnumerable<Permission>> GetPermissionsAsync()
=> Task.FromResult(_allPermissions);
public IEnumerable<PermissionStereotype> GetDefaultStereotypes() =>
[
new PermissionStereotype
{
Name = OrchardCoreConstants.Roles.Administrator,
Permissions = _allPermissions,
},
];
}
Register it with services.AddPermissionProvider<Permissions>();. The third constructor argument lists permissions that imply this one. Display a description with IStringLocalizerFactory.Localize(permission.Description); read the raw text with permission.Description.Value.
Checking Permissions in a Controller
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OrchardCore.ContentManagement;
using OrchardCore.Contents;
namespace MyModule.Controllers;
public sealed class InvoiceController : Controller
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentManager _contentManager;
public InvoiceController(
IAuthorizationService authorizationService,
IContentManager contentManager)
{
_authorizationService = authorizationService;
_contentManager = contentManager;
}
public async Task<IActionResult> Display(string contentItemId)
{
var contentItem = await _contentManager.GetAsync(contentItemId);
if (contentItem == null)
{
return NotFound();
}
if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ViewContent, contentItem))
{
return Forbid();
}
return View(contentItem);
}
}
Custom Authorization Handler
using Microsoft.AspNetCore.Authorization;
using OrchardCore.ContentManagement;
using OrchardCore.Security;
namespace MyModule.Security;
public sealed class InvoiceOwnerAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
if (requirement.Permission.Name == InvoicePermissions.ViewInvoices.Name &&
context.Resource is ContentItem contentItem &&
contentItem.Owner == context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Register with services.AddScoped<IAuthorizationHandler, InvoiceOwnerAuthorizationHandler>();. ContentItem.Owner stores the user ID.
API Endpoints and Error Responses
Secure API endpoints with the Api scheme and return Problem Details:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OrchardCore;
namespace MyModule.Controllers;
[ApiController]
[Route("api/invoices")]
[Authorize(AuthenticationSchemes = OrchardCoreConstants.AuthenticationSchemes.Api)]
public sealed class InvoiceApiController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
public InvoiceApiController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
if (!await _authorizationService.AuthorizeAsync(User, InvoicePermissions.ViewInvoices))
{
return this.ApiForbidProblem();
}
return this.ApiNotFoundProblem();
}
}
- In 4.0, the
401and403responses produced by theApischeme, and error responses of endpoints marked[SkipStatusCodePages], carry an RFC 9457application/problem+jsonbody. Other error responses still go to theOrchardCore.Diagnosticserror pages. - Status codes and headers are unchanged;
WWW-Authenticate(RFC 6750 error details) still comes from the authentication handler. The body is written throughIProblemDetailsService, soservices.AddProblemDetails(options => ...)customizations apply.AddOrchardCms()enables it; hosts built withAddOrchardCore()call.AddApiProblemDetails(). - Helpers on
ControllerBaseandHttpContext:ApiForbidProblem(),ApiBadRequestProblem(),ApiNotFoundProblem(),ApiValidationProblem(). In minimal APIs,httpContext.ChallengeOrForbid(OrchardCoreConstants.AuthenticationSchemes.Api)returns the right challenge. - Add
[SkipStatusCodePages](or.WithMetadata(new SkipStatusCodePagesAttribute())) to JSON endpoints that use cookie authentication so their errors are not replaced by HTML pages.
Anti-Forgery
Orchard Core forms include anti-forgery tokens automatically. Only bypass validation for endpoints that authenticate with bearer tokens:
[HttpPost]
[IgnoreAntiforgeryToken]
[Authorize(AuthenticationSchemes = OrchardCoreConstants.AuthenticationSchemes.Api)]
public IActionResult Import()
{
return Ok();
}
Minimal APIs use .DisableAntiforgery() together with the Api scheme.
Upgrading from 3.x
- Obsolete permission members were removed; role claims are unchanged. Replace, for example:
OrchardCore.Users.CommonPermissions.*→OrchardCore.Users.UsersPermissions.*OrchardCore.Deployment.CommonPermissions.*→OrchardCore.Deployment.DeploymentPermissions.*OrchardCore.Apis.GraphQL.CommonPermissions.*→OrchardCore.Apis.GraphQL.GraphQLPermissions.*OrchardCore.Media.Permissions.*→OrchardCore.Media.MediaPermissions.*OrchardCore.OpenId.Permissions.*→OrchardCore.OpenId.OpenIdPermissions.*OrchardCore.Settings.Permissions.ManageSettings→OrchardCore.Settings.SettingsPermissions.ManageSettingsOrchardCore.Security.StandardPermissions.SiteOwner→ removed; assign users to theAdministratorrole.
- Build against the latest 3.x first and fix every
CS0618warning. Use the member with the same name, not the one named in the old obsolete message (AdminMenuManageAdminMenu/ViewAdminMenuAllandCommonPermissions.CreateAssignRoleToUsersPermissionreturned the wrong permission in 3.x). Permission.Descriptionchanged fromstringtoLocalizationSource; recompile modules. Translations of built-in descriptions now come from PO files, not Data Localization.- The
OrchardCore_SecurityandOrchardCore_ReverseProxyconfiguration sections are nowOrchardCore:SecurityandOrchardCore:ReverseProxy(old names still work but are deprecated). - API
401/403responses now have a Problem Details body instead of an empty one.