Describe the suggested improvement
Is your improvement related to a problem? Please describe.
ServiceControl owns the OIDC configuration that ServicePulse authenticates with. AuthenticationController.Configuration() bootstraps the client from OpenIdConnectSettings.
Every value ServicePulse needs comes from here, except one. The requested scope string is assembled client-side, and offline_access is hard-coded into it:
// ServicePulse: src/Frontend/src/stores/AuthStore.ts:64
scope: `${apiScope} openid profile email offline_access`,
So an operator can configure the authority, client ID, audience and API scopes through Authentication.ServicePulse.*, but cannot stop ServicePulse asking for offline_access. That is the one part of the OIDC contract ServiceControl does not bootstrap, and it assumes every identity provider permits the scope.
Not all do, and the failure is total rather than degraded. When an IdP disallows offline_access, the whole authorization request is rejected and the user never reaches a login page.
Describe the suggested solution
Bootstrap the entire scope string from ServiceControl, defaulting to today's behaviour. ServicePulse should assemble no part of it.
if (requireServicePulseSettings)
{
...
ServicePulseOfflineAccessScopeEnabled = SettingsReader.Read(rootNamespace, "Authentication.ServicePulse.OfflineAccessScopeEnabled", true);
}
Defaulting to true means existing deployments are unchanged, and the setting needs no validation rule.
src/ServiceControl/Authentication/AuthenticationController.cs - compose the complete scope string and expose it on AuthConfig, so ServicePulse uses it:
// e.g. "access_as_user openid profile email offline_access", or the same without the
// trailing 'offline_access' scope when OfflineAccessScopeEnabled is false
Scopes = settings.OpenIdConnectSettings.ServicePulseScopes
The other scopes remain fixed, but they are still bootstrapped. They are deliberately not exposed as settings, because they are required:
openid is what makes this an OpenID Connect request at all. Without it no ID token is issued, and ServicePulse cannot establish a session.
profile carries preferred_username, which is the default Authentication.SubjectNameClaim.
email carries the email claim, which operators may legitimately point Authentication.SubjectNameClaim at instead. Removing it would silently narrow that existing choice.
offline_access is the only scope in the string whose availability genuinely varies by provider and by client, and unlike the other three, ServicePulse should remains functional without it.
A companion change in the ServicePulse repo is required to use the composed string in place of the hard-coded template at AuthStore.ts. When the field is absent from an older ServiceControl, ServicePulse should fall back to its current behaviour of assembling api_scopes plus the four scopes.
Describe alternatives you've considered
Leave it hard-coded and document the IdP requirement. Quickest. Rejected because the failure mode is a total sign-in block with an error that misidentifies the cause, so the cost of hitting it is high even at low probability.
Have ServicePulse detect error=invalid_scope and retry without the scope. Needs no ServiceControl change, but burns a redirect round-trip on every sign-in for affected deployments, and invalid_scope does not identify which scope failed i.e. the retry would be guessing.
Additional Context
Disabling offline_access is generally not recommended, which is why it should remain Enabled by default.
Without offline_access some IdPs dont issue a refresh token, so ServicePulse cannot renew over a back-channel call to the token endpoint. It falls back to a hidden iframe against the silent-renew URI, which depends on the IdP session cookie being sent in a third-party context. Browsers that restrict third-party cookies (Safari ITP, Brave, and Chrome's ongoing restrictions) will fail that request.
When it fails, ServicePulse re-authenticates with a full-page redirect at every token expiry:
- With a live IdP session this is a round-trip and a reload, so it is brief, but in-app state is lost.
- Without one, the user is returned to the login page mid-session.
Describe the suggested improvement
Is your improvement related to a problem? Please describe.
ServiceControl owns the OIDC configuration that ServicePulse authenticates with.
AuthenticationController.Configuration()bootstraps the client fromOpenIdConnectSettings.Every value ServicePulse needs comes from here, except one. The requested scope string is assembled client-side, and
offline_accessis hard-coded into it:So an operator can configure the authority, client ID, audience and API scopes through
Authentication.ServicePulse.*, but cannot stop ServicePulse asking foroffline_access. That is the one part of the OIDC contract ServiceControl does not bootstrap, and it assumes every identity provider permits the scope.Not all do, and the failure is total rather than degraded. When an IdP disallows
offline_access, the whole authorization request is rejected and the user never reaches a login page.Describe the suggested solution
Bootstrap the entire scope string from ServiceControl, defaulting to today's behaviour. ServicePulse should assemble no part of it.
Defaulting to
truemeans existing deployments are unchanged, and the setting needs no validation rule.src/ServiceControl/Authentication/AuthenticationController.cs- compose the complete scope string and expose it onAuthConfig, so ServicePulse uses it:The other scopes remain fixed, but they are still bootstrapped. They are deliberately not exposed as settings, because they are required:
openidis what makes this an OpenID Connect request at all. Without it no ID token is issued, and ServicePulse cannot establish a session.profilecarriespreferred_username, which is the defaultAuthentication.SubjectNameClaim.emailcarries theemailclaim, which operators may legitimately pointAuthentication.SubjectNameClaimat instead. Removing it would silently narrow that existing choice.offline_accessis the only scope in the string whose availability genuinely varies by provider and by client, and unlike the other three, ServicePulse should remains functional without it.A companion change in the ServicePulse repo is required to use the composed string in place of the hard-coded template at
AuthStore.ts. When the field is absent from an older ServiceControl, ServicePulse should fall back to its current behaviour of assemblingapi_scopesplus the four scopes.Describe alternatives you've considered
Leave it hard-coded and document the IdP requirement. Quickest. Rejected because the failure mode is a total sign-in block with an error that misidentifies the cause, so the cost of hitting it is high even at low probability.
Have ServicePulse detect
error=invalid_scopeand retry without the scope. Needs no ServiceControl change, but burns a redirect round-trip on every sign-in for affected deployments, andinvalid_scopedoes not identify which scope failed i.e. the retry would be guessing.Additional Context
Disabling
offline_accessis generally not recommended, which is why it should remainEnabledby default.Without
offline_accesssome IdPs dont issue a refresh token, so ServicePulse cannot renew over a back-channel call to the token endpoint. It falls back to a hidden iframe against the silent-renew URI, which depends on the IdP session cookie being sent in a third-party context. Browsers that restrict third-party cookies (Safari ITP, Brave, and Chrome's ongoing restrictions) will fail that request.When it fails, ServicePulse re-authenticates with a full-page redirect at every token expiry: