You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #1475 (#1475) adds opt-in
OAuth bearer-token authentication to SPE remoting. The PR targets master,
but the feature/remoting branch has already diverged substantially in every
file the PR touches, so the diff cannot be cherry-picked as-is. This issue
tracks re-implementing the PR's intent on top of feature/remoting, using the
helpers and hardening already present on that branch.
.nuget/nuget.exe (~5 MB binary) is committed by the PR; this should not
land.
PR uses em-dashes in config comments and C# XML docs, which the project
style rule forbids.
Scope
Client (Modules/SPE/): add -AccessToken as a first-class auth mode
alongside Username/Password, SharedSecret (HMAC JWT), and AccessKeyId.
Server (src/Spe/Core/Settings/Authorization/): add OAuthBearerTokenAuthenticationProvider that validates external JWTs
(exp, nbf, iat, aud, iss, scope, username claim), with JWT signature
verification via a configurable JWKS endpoint. Ship two env-var-gated Sitecore
config files: one to enable the provider, one to enable remoting endpoints.
Out of scope for this issue:
Resurrecting Invoke-RemoteScriptAsync.
Wiring item-based remoting policies (RemotingPolicyManager) to apply to
bearer-token sessions. Deferred; leave an inline TODO and a follow-up
issue.
.nuget/nuget.exe.
Design decisions
Question
Decision
Item-based remoting policies for bearer sessions
Defer. Bypass for now with inline TODO.
JWT signature verification
JWKS on day one. SkipSignatureValidation remains as an explicit, loudly-logged opt-out; not the default.
Role scope on Spe.XMCloud.Remoting.config
Standalone or ContentManagement or XMCloud (not XMCloud-only), so non-cloud operators can opt in by env var.
Client changes
Modules/SPE/SPE.psm1
Extend Expand-ScriptSession (lines 1-16) to surface AccessToken.
Extend New-SpeHttpClient (lines 18-67) with [string]$AccessToken. Add a
3-way branch: AccessToken wins, then SharedSecret (HMAC JWT), then Basic.
No JWT is minted client-side for bearer tokens because they are externally
issued.
Modules/SPE/New-ScriptSession.ps1
New parameter set "AccessToken" with mandatory [string]$AccessToken.
Add "AccessToken" = [string]$AccessToken to the session hashtable.
Update .PARAMETER docs and add an .EXAMPLE for bearer-token use.
Add [string]$AccessToken to the param block (valid in Uri and Session
sets).
In the Session branch, unpack $sd.AccessToken when the caller did not
pass one explicitly.
Pass -AccessToken $AccessToken to New-SpeHttpClient.
Update docstrings.
Server changes
Refactor: extract shared JWT claim helpers
Lift these private helpers from SharedSecretAuthenticationProvider.cs:143-222 into a new Core/Settings/Authorization/JwtClaimValidator.cs (internal static): IsValidExpiration, IsValidNotBefore, IsValidIssuedAt, IsValidTokenLifetime, IsValidAudience, IsValidIssuer, Decode. Both
providers use them. No behaviour change; ship in its own commit.
Implements ISpeAuthenticationProviderEx (not only the base interface).
Configuration properties:
Property
Default
AllowedAudiences
empty
AllowedIssuers
empty
RequiredScopes
empty
UsernameClaim
sub
ServiceAccountUsername
null
AllowedAlgorithms
RS256,RS384,RS512,ES256
JwksUri
null
JwksCacheSeconds
600
SkipSignatureValidation
false
MaxTokenLifetimeSeconds
3600
DetailedAuthenticationErrors
false
SuppressWarnings
false
ClockSkewSeconds
30
Validation pipeline (short-circuit on first failure):
IsJwtShape - three dot-separated parts.
Decode header, typ == "JWT", alg in allowlist.
Decode payload JSON.
exp, nbf, iat present and within bounds (via JwtClaimValidator).
iss in AllowedIssuers.
aud in AllowedAudiences (support string or array shape).
Scope check - read scope (space-delimited string) or scp (array);
assert each RequiredScopes entry is present, case-insensitive.
Signature: JwksKeyResolver.GetKey(kid) then verify over header.payload. When SkipSignatureValidation=true, log a Warn [OAuthBearer] action=signatureSkipped and continue.
Username: ServiceAccountUsername if set, else payload[UsernameClaim]?.ToString(). Fail if null/empty.
Log format matches the existing provider: [JWT] action=validationFailed reason=... via PowerShellLog.Warn, with LogSanitizer.SanitizeValue on any user-sourced value.
Three <sitecore> blocks, each gated on its own env var
(SITECORE_SPE_REMOTING_ENABLED, SITECORE_SPE_REMOTING_FILE_ENABLED, SITECORE_SPE_REMOTING_MEDIA_ENABLED) via env:require.
Role scope: Standalone or ContentManagement or XMCloud.
ASCII-only punctuation.
src/Spe/Spe.csproj
Wire the new .config and .cs files.
Suggested commit structure
Refactor: extract JwtClaimValidator from SharedSecretAuthenticationProvider. No behaviour change.
Client: add AccessToken to New-SpeHttpClient, Expand-ScriptSession, New-ScriptSession, Invoke-RemoteScript, Send-RemoteItem, Receive-RemoteItem.
Docs: update docstrings and any touched README.
Each commit is independently buildable. (1) and (3) are independently useful.
Verification
task build after each commit.
task up && task deploy.
With SITECORE_SPE_OAUTH unset: existing shared-secret and API-key flows
still work; log shows SharedSecretAuthenticationProvider active.
With SITECORE_SPE_OAUTH=true and a JWKS + audiences + issuers populated:
OAuth provider active; a valid test token (RS256, matching iss/aud/ scope) authenticates for all four remoting cmdlets.
Negative matrix - each must 401 and emit a distinct [JWT] action=validationFailed reason=... log line:
expired exp, future nbf, future iat, lifetime over max, wrong iss,
wrong aud, missing scope, alg not in allowlist, tampered signature,
missing username claim, unresolvable kid.
SkipSignatureValidation=true emits a Warn on every request.
tests/integration/Run-RemotingTests.ps1 green.
Follow-ups
Separate issue: wire RemotingPolicyManager for bearer-token sessions.
Separate issue: consider Azure-AD-specific issuer formatting quirks if
they surface in testing.
Context
PR #1475 (#1475) adds opt-in
OAuth bearer-token authentication to SPE remoting. The PR targets
master,but the
feature/remotingbranch has already diverged substantially in everyfile the PR touches, so the diff cannot be cherry-picked as-is. This issue
tracks re-implementing the PR's intent on top of
feature/remoting, using thehelpers and hardening already present on that branch.
Conflict points with
feature/remoting:New-SpeHttpClientand
Expand-ScriptSession(Modules/SPE/SPE.psm1:1-70); PR Implement OAuth bearer token support and improve AccessToken handling #1475 inlinesit in each cmdlet.
Invoke-RemoteScriptAsyncwas removed (commit6d3bbee2d, Remove Invoke-RemoteScriptAsync (dead code) #1414); the PRstill modifies it.
New-ScriptSession.ps1already has anAccessKeyparameter set; a newAccessTokenset must slot in alongside it.SharedSecretAuthenticationProvider.cswas hardened with algorithmallowlist, iat/nbf, clock skew, min secret length, issuer validation
(Enhance JWT authentication for MCP server compatibility #1420, Remoting Policies — item-based CLM, command allowlists, and approved scripts for SPE remoting #1426, Add explicit algorithm allowlist to JWT validation #1445). The PR's
OAuthBearerTokenAuthenticationProviderregresses on all of those and skips signature verification entirely.
.nuget/nuget.exe(~5 MB binary) is committed by the PR; this should notland.
style rule forbids.
Scope
Client (
Modules/SPE/): add-AccessTokenas a first-class auth modealongside Username/Password, SharedSecret (HMAC JWT), and AccessKeyId.
Server (
src/Spe/Core/Settings/Authorization/): addOAuthBearerTokenAuthenticationProviderthat validates external JWTs(exp, nbf, iat, aud, iss, scope, username claim), with JWT signature
verification via a configurable JWKS endpoint. Ship two env-var-gated Sitecore
config files: one to enable the provider, one to enable remoting endpoints.
Out of scope for this issue:
Invoke-RemoteScriptAsync.RemotingPolicyManager) to apply tobearer-token sessions. Deferred; leave an inline TODO and a follow-up
issue.
.nuget/nuget.exe.Design decisions
SkipSignatureValidationremains as an explicit, loudly-logged opt-out; not the default.Spe.XMCloud.Remoting.configStandalone or ContentManagement or XMCloud(not XMCloud-only), so non-cloud operators can opt in by env var.Client changes
Modules/SPE/SPE.psm1Expand-ScriptSession(lines 1-16) to surfaceAccessToken.New-SpeHttpClient(lines 18-67) with[string]$AccessToken. Add a3-way branch: AccessToken wins, then SharedSecret (HMAC JWT), then Basic.
No JWT is minted client-side for bearer tokens because they are externally
issued.
Modules/SPE/New-ScriptSession.ps1"AccessToken"with mandatory[string]$AccessToken."AccessToken" = [string]$AccessTokento the session hashtable..PARAMETERdocs and add an.EXAMPLEfor bearer-token use.Modules/SPE/Invoke-RemoteScript.ps1,Send-RemoteItem.ps1,Receive-RemoteItem.ps1[string]$AccessTokento the param block (valid inUriandSessionsets).
Sessionbranch, unpack$sd.AccessTokenwhen the caller did notpass one explicitly.
-AccessToken $AccessTokentoNew-SpeHttpClient.Server changes
Refactor: extract shared JWT claim helpers
Lift these private helpers from
SharedSecretAuthenticationProvider.cs:143-222into a newCore/Settings/Authorization/JwtClaimValidator.cs(internal static):IsValidExpiration,IsValidNotBefore,IsValidIssuedAt,IsValidTokenLifetime,IsValidAudience,IsValidIssuer,Decode. Bothproviders use them. No behaviour change; ship in its own commit.
Core/Settings/Authorization/JwksKeyResolver.cs(new)JwksUri, parsekeys[], index bykid.JwksUriwith configurable TTL (default 600s); refresh on miss.RSAParameters/ECParametersfor a givenkid+alg.Core/Settings/Authorization/OAuthBearerTokenAuthenticationProvider.cs(new)Implements
ISpeAuthenticationProviderEx(not only the base interface).Configuration properties:
AllowedAudiencesAllowedIssuersRequiredScopesUsernameClaimsubServiceAccountUsernameAllowedAlgorithmsRS256,RS384,RS512,ES256JwksUriJwksCacheSecondsSkipSignatureValidationMaxTokenLifetimeSecondsDetailedAuthenticationErrorsSuppressWarningsClockSkewSecondsValidation pipeline (short-circuit on first failure):
IsJwtShape- three dot-separated parts.typ == "JWT",algin allowlist.exp,nbf,iatpresent and within bounds (viaJwtClaimValidator).issinAllowedIssuers.audinAllowedAudiences(support string or array shape).scope(space-delimited string) orscp(array);assert each
RequiredScopesentry is present, case-insensitive.JwksKeyResolver.GetKey(kid)then verify overheader.payload. WhenSkipSignatureValidation=true, log a Warn[OAuthBearer] action=signatureSkippedand continue.ServiceAccountUsernameif set, elsepayload[UsernameClaim]?.ToString(). Fail if null/empty.Log format matches the existing provider:
[JWT] action=validationFailed reason=...viaPowerShellLog.Warn, withLogSanitizer.SanitizeValueon any user-sourced value.src/Spe/App_Config/Include/Spe/Spe.OAuthBearer.config(new)env:require="SITECORE_SPE_OAUTH".role:require="Standalone or ContentManagement or XMCloud".<allowedAudiences>,<allowedIssuers>,<allowedAlgorithms>,<requiredScopes>,<usernameClaim>,<serviceAccountUsername>,<jwksUri>,<jwksCacheSeconds>,<skipSignatureValidation>,<maxTokenLifetimeSeconds>,<detailedAuthenticationErrors>.src/Spe/App_Config/Include/Spe/Spe.XMCloud.Remoting.config(new)<sitecore>blocks, each gated on its own env var(
SITECORE_SPE_REMOTING_ENABLED,SITECORE_SPE_REMOTING_FILE_ENABLED,SITECORE_SPE_REMOTING_MEDIA_ENABLED) viaenv:require.Standalone or ContentManagement or XMCloud.src/Spe/Spe.csprojWire the new
.configand.csfiles.Suggested commit structure
JwtClaimValidatorfromSharedSecretAuthenticationProvider. No behaviour change.JwksKeyResolver+OAuthBearerTokenAuthenticationProvider+both config files + csproj wiring.
AccessTokentoNew-SpeHttpClient,Expand-ScriptSession,New-ScriptSession,Invoke-RemoteScript,Send-RemoteItem,Receive-RemoteItem.Each commit is independently buildable. (1) and (3) are independently useful.
Verification
task buildafter each commit.task up && task deploy.SITECORE_SPE_OAUTHunset: existing shared-secret and API-key flowsstill work; log shows
SharedSecretAuthenticationProvideractive.SITECORE_SPE_OAUTH=trueand a JWKS + audiences + issuers populated:OAuth provider active; a valid test token (
RS256, matchingiss/aud/scope) authenticates for all four remoting cmdlets.[JWT] action=validationFailed reason=...log line:expired
exp, futurenbf, futureiat, lifetime over max, wrongiss,wrong
aud, missing scope,algnot in allowlist, tampered signature,missing username claim, unresolvable
kid.SkipSignatureValidation=trueemits a Warn on every request.tests/integration/Run-RemotingTests.ps1green.Follow-ups
RemotingPolicyManagerfor bearer-token sessions.they surface in testing.
References
Invoke-RemoteScriptAsyncremoval: Remove Invoke-RemoteScriptAsync (dead code) #1414