orchardcore-openid
Skill for configuring and managing OpenID Connect in Orchard Core. Covers server setup, client application registration, authorization flows, token validation, external authentication providers, JWT b
SKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-openid/SKILL.md
GitHubOrchard Core OpenID Connect - Prompt Templates
Module Overview
The OrchardCore.OpenId module provides OpenID Connect and OAuth 2.0 on top of OpenIddict (7.7.1 in Orchard Core 4.0). Enable the features your scenario needs:
| Feature | ID | Purpose |
|---|---|---|
| OpenID Connect Core Services | OrchardCore.OpenId |
Shared stores and managers. |
| OpenID Connect Management UI | OrchardCore.OpenId.Management |
Applications and scopes admin. |
| OpenID Connect Authorization Server | OrchardCore.OpenId.Server |
/connect/authorize, /connect/token, /connect/logout, /connect/userinfo, /connect/par, introspection, revocation. Depends on Management. |
| OpenID Connect Token Validation | OrchardCore.OpenId.Validation |
Validates bearer tokens for the Api scheme (same tenant, another tenant, or a remote authority). |
| OpenID Connect Client Integration | OrchardCore.OpenId.Client |
Sign in with an external OIDC provider. Depends on OrchardCore.Users.ExternalAuthentication. |
Admin locations:
- Settings → OpenID Connect → Authorization Server (
ManageServerSettings) - Settings → OpenID Connect → Token Validation (
ManageValidationSettings) - Settings → OpenID Connect → Authentication Client (
ManageClientSettings) - Access Control → OpenID Connect → Applications and Scopes (
ManageApplications,ManageScopes)
Guidelines
- The authorization server validates tokens sent to its own
/connect/userinfoendpoint; enableOrchardCore.OpenId.Validationto accept its tokens on API endpoints. - Use authorization code + PKCE for browser and native apps, client credentials for services. Avoid the password and implicit flows for new clients.
- Always use HTTPS in production for every redirect URI and the authority.
- Secure API endpoints with
[Authorize(AuthenticationSchemes = OrchardCoreConstants.AuthenticationSchemes.Api)](the"Api"scheme) rather than the raw OpenIddict scheme; the token's roles then drive Orchard permission checks. - All recipe JSON must be wrapped in the root
{ "steps": [...] }format. - All C# classes must use the
sealedmodifier, except View Models.
Authorization Server Settings
OpenIdServerSettings Recipe Step
The dedicated step toggles endpoints with Enable*Endpoint booleans (the paths are always /connect/*). It replaces the server settings: every flow or endpoint left out becomes false, while RequireEndSessionConfirmation defaults to true.
{
"steps": [
{
"name": "feature",
"enable": [
"OrchardCore.OpenId.Server",
"OrchardCore.OpenId.Validation"
]
},
{
"name": "OpenIdServerSettings",
"AccessTokenFormat": "JsonWebToken",
"Authority": "https://auth.example.com",
"EnableTokenEndpoint": true,
"EnableAuthorizationEndpoint": true,
"EnableLogoutEndpoint": true,
"EnableUserInfoEndpoint": true,
"EnablePushedAuthorizationEndpoint": true,
"EnableIntrospectionEndpoint": false,
"EnableRevocationEndpoint": false,
"AllowAuthorizationCodeFlow": true,
"AllowClientCredentialsFlow": true,
"AllowRefreshTokenFlow": true,
"AllowPasswordFlow": false,
"AllowImplicitFlow": false,
"AllowHybridFlow": false,
"DisableRollingRefreshTokens": false,
"RequireProofKeyForCodeExchange": true,
"RequirePushedAuthorizationRequests": false,
"RequireEndSessionConfirmation": true,
"UseReferenceAccessTokens": false
}
]
}
Other properties: DisableAccessTokenEncryption (lets third-party resource servers read JWTs), SigningCertificateStoreLocation/SigningCertificateStoreName/SigningCertificateThumbprint, and the matching EncryptionCertificate* properties. AccessTokenFormat is DataProtection (default) or JsonWebToken.
The generic settings step also accepts an OpenIdServerSettings object with the stored property names, including custom *EndpointPath values.
Server Settings via Migration
using OrchardCore.Data.Migration;
using OrchardCore.OpenId.Services;
namespace MyModule.Migrations;
public sealed class OpenIdServerMigrations : DataMigration
{
private readonly IOpenIdServerService _serverService;
public OpenIdServerMigrations(IOpenIdServerService serverService)
{
_serverService = serverService;
}
public async Task<int> CreateAsync()
{
var settings = await _serverService.LoadSettingsAsync();
settings.AllowAuthorizationCodeFlow = true;
settings.AllowClientCredentialsFlow = true;
settings.AllowRefreshTokenFlow = true;
settings.RequireProofKeyForCodeExchange = true;
settings.AuthorizationEndpointPath = "/connect/authorize";
settings.TokenEndpointPath = "/connect/token";
settings.LogoutEndpointPath = "/connect/logout";
settings.UserinfoEndpointPath = "/connect/userinfo";
settings.PushedAuthorizationEndpointPath = "/connect/par";
settings.RequireEndSessionConfirmation = true;
await _serverService.UpdateSettingsAsync(settings);
return 1;
}
}
Use LoadSettingsAsync() (not the cached GetSettingsAsync()) before updating. Validate with ValidateSettingsAsync(settings); for example, PAR requires the authorization endpoint, and the code flow requires both the authorization and token endpoints.
Logout Without Confirmation
RequireEndSessionConfirmation (default true) shows a confirmation prompt on /connect/logout. Set it to false to sign users out immediately when the request carries a valid id_token_hint issued for the currently authenticated user; otherwise the prompt is still shown. When the prompt is skipped, the local authentication cookie is removed too. The setting is available in the admin, the OpenIdServerSettings recipe step, and deployment plans.
{
"steps": [
{
"name": "OpenIdServerSettings",
"EnableTokenEndpoint": true,
"EnableAuthorizationEndpoint": true,
"EnableLogoutEndpoint": true,
"EnableUserInfoEndpoint": true,
"AllowAuthorizationCodeFlow": true,
"AllowRefreshTokenFlow": true,
"RequireProofKeyForCodeExchange": true,
"RequireEndSessionConfirmation": false
}
]
}
Token Endpoint Rate Limiting
When OrchardCore.RateLimits is enabled, the server registers a built-in limit on POST /connect/token (route Access.Token): 10 requests per minute per IP, sliding window, sharing the password-authentication budget with the Users login form. Rejected calls return 429. See orchardcore-rate-limits.
Client Applications
Guidelines
- Every client needs a unique
ClientId. Confidential clients need aClientSecret; public clients (SPAs, mobile) must not have one and should require PKCE. ConsentTypeisexplicit,implicit,external, orsystematic. Useimplicitonly for trusted first-party apps.- Register only the redirect URIs the app uses. Multiple URIs are space- or comma-separated in recipes.
- Client credentials apps get their permissions from
RoleEntries; user-based flows get them from the user's roles through therolesscope.
OpenIdApplication Recipe Step
One application per step:
{
"steps": [
{
"name": "OpenIdApplication",
"ClientId": "spa-client",
"DisplayName": "Single Page Application",
"Type": "public",
"ConsentType": "implicit",
"AllowAuthorizationCodeFlow": true,
"AllowRefreshTokenFlow": true,
"AllowLogoutEndpoint": true,
"RequireProofKeyForCodeExchange": true,
"RedirectUris": "https://spa.example.com/callback https://spa.example.com/silent-renew",
"PostLogoutRedirectUris": "https://spa.example.com/logout-callback",
"ScopeEntries": [
{ "Name": "profile" },
{ "Name": "email" },
{ "Name": "roles" },
{ "Name": "api" }
]
},
{
"name": "OpenIdApplication",
"ClientId": "background-worker",
"DisplayName": "Background Worker",
"Type": "confidential",
"ClientSecret": "{{WorkerSecret}}",
"AllowClientCredentialsFlow": true,
"RoleEntries": [
{ "Name": "Editor" }
],
"ScopeEntries": [
{ "Name": "api" }
]
}
]
}
Other flags: AllowPasswordFlow, AllowHybridFlow, AllowImplicitFlow, AllowIntrospectionEndpoint, AllowRevocationEndpoint, RequirePushedAuthorizationRequests. The openid scope does not need a scope entry. A recipe run for an existing ClientId updates that application.
Client Registration via Migration
using OpenIddict.Abstractions;
using OrchardCore.Data.Migration;
using OrchardCore.OpenId.Abstractions.Descriptors;
using OrchardCore.OpenId.Abstractions.Managers;
namespace MyModule.Migrations;
public sealed class OpenIdClientMigrations : DataMigration
{
private readonly IOpenIdApplicationManager _applicationManager;
public OpenIdClientMigrations(IOpenIdApplicationManager applicationManager)
{
_applicationManager = applicationManager;
}
public async Task<int> CreateAsync()
{
await _applicationManager.CreateAsync(new OpenIdApplicationDescriptor
{
ClientId = "my-web-app",
DisplayName = "My Web Application",
ClientSecret = "your-client-secret-here",
ClientType = OpenIddictConstants.ClientTypes.Confidential,
ConsentType = OpenIddictConstants.ConsentTypes.Implicit,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.EndSession,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
},
Requirements =
{
OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange,
},
RedirectUris = { new Uri("https://myapp.example.com/signin-oidc") },
PostLogoutRedirectUris = { new Uri("https://myapp.example.com/signout-callback-oidc") },
});
return 1;
}
}
Prefer a recipe for secrets that differ per environment, and never commit real secrets.
Authorization Flows
| Flow | Server setting | Client requirements |
|---|---|---|
| Authorization code (+ PKCE) | AllowAuthorizationCodeFlow |
GrantTypes.AuthorizationCode, ResponseTypes.Code; public clients require PKCE. |
| Client credentials | AllowClientCredentialsFlow |
Confidential client with GrantTypes.ClientCredentials; roles from RoleEntries. |
| Refresh token | AllowRefreshTokenFlow |
GrantTypes.RefreshToken; rolling refresh tokens unless DisableRollingRefreshTokens. |
| Pushed authorization requests | EnablePushedAuthorizationEndpoint (/connect/par) |
Application RequirePushedAuthorizationRequests to enforce. |
| Implicit / hybrid / password | AllowImplicitFlow, AllowHybridFlow, AllowPasswordFlow |
Legacy only. |
Scopes and Claims
OpenIdScope Recipe Step
{
"steps": [
{
"name": "OpenIdScope",
"ScopeName": "api",
"DisplayName": "API Access",
"Description": "Grants access to protected API endpoints.",
"Resources": "my-resource-server"
}
]
}
Resources is space-separated and becomes the token audience. In the admin, a scope can also target tenants, which builds oct:{TenantName} audiences for cross-tenant validation.
Standard Scopes
openid— required; returnssub.profile— profile claims.email—emailandemail_verified.phone— phone number claims.roles— role claims; needed for Orchard permission checks on API calls.
Custom Userinfo Claims
using OrchardCore.OpenId.Abstractions.Handlers;
namespace MyModule.Services;
public sealed class TenantUserInfoClaimsProvider : IUserInfoClaimsProvider
{
public Task GenerateAsync(UserInfoClaimsContext context)
{
context.Claims["tenant"] = "example";
return Task.CompletedTask;
}
}
Register with services.AddScoped<IUserInfoClaimsProvider, TenantUserInfoClaimsProvider>();. Standard scope claims are already written when GenerateAsync runs; inspect context.Principal for the user.
Securing APIs with Token Validation
OpenIdValidationSettings Recipe Step
Set exactly one of Tenant (validate tokens issued by an authorization server tenant in the same app) or Authority (remote server with discovery). With neither, the settings are invalid and every bearer call returns 401.
{
"steps": [
{
"name": "OpenIdValidationSettings",
"Tenant": "Default"
}
]
}
{
"steps": [
{
"name": "OpenIdValidationSettings",
"Authority": "https://idp.example.com",
"Audience": "my-resource-server",
"MetadataAddress": "",
"DisableTokenTypeValidation": false
}
]
}
API Controller
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OrchardCore;
using OrchardCore.Contents;
namespace MyModule.Controllers;
[ApiController]
[Route("api/products")]
[Authorize(AuthenticationSchemes = OrchardCoreConstants.AuthenticationSchemes.Api)]
public sealed class ProductsController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
public ProductsController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ViewContent))
{
return this.ApiForbidProblem();
}
return Ok(new { UserId = User.FindFirstValue(ClaimTypes.NameIdentifier) });
}
}
- A missing or invalid token returns
401with the RFC 6750WWW-Authenticateheader from OpenIddict and, in 4.0, an RFC 9457 Problem Details body. Insufficient permissions return403with Problem Details. DisableAntiforgery()/[IgnoreAntiforgeryToken]is appropriate only on bearer-authenticated endpoints.
External OpenID Connect Login
The OrchardCore.OpenId.Client feature signs users in with an external provider and links or registers local users (an email claim links to an existing account after authentication).
{
"steps": [
{
"name": "OpenIdClientSettings",
"DisplayName": "Corporate IdP",
"Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"ClientId": "azure-app-client-id",
"ClientSecret": "azure-app-client-secret",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"SignedOutRedirectUri": "https://www.example.com/",
"ResponseType": "code",
"ResponseMode": "form_post",
"Scopes": "email profile",
"StoreExternalTokens": false
}
]
}
Scopes is a space- or comma-separated string of extra scopes. For Microsoft Entra ID, Google, GitHub, Facebook, or X, prefer the dedicated modules (orchardcore-microsoft-auth, orchardcore-google, orchardcore-github-authentication, orchardcore-facebook, orchardcore-twitter).
Upgrading from 3.x
OrchardCore.OpenId.Permissions.ManageApplications,ManageClientSettings,ManageScopes,ManageServerSettings, andManageValidationSettingswere removed; use the same members onOrchardCore.OpenId.OpenIdPermissions.- Disabling
RequireEndSessionConfirmationnow really skips the prompt for a validid_token_hint(the prompt was always shown in 3.x). It can now be set from theOpenIdServerSettingsstep and is exported by deployment plans; omitted, it istrue. - Recipes that used non-existent step properties (
UseRollingRefreshTokens,*EndpointPathin theOpenIdServerSettingsstep,ClientTypeorAllowedScopesinOpenIdApplication) must useDisableRollingRefreshTokens,Enable*Endpoint,Type, andScopeEntries. - API
401/403challenges now carry a Problem Details body; the status codes andWWW-Authenticateheader are unchanged. - With Rate Limits enabled,
POST /connect/tokenis limited per IP.
See openid-examples.md for complete setups.