Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/authentication-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ set SERVICECONTROL_AUTHENTICATION_AUDIENCE=api://servicecontrol-test
set SERVICECONTROL_AUTHENTICATION_SERVICEPULSE_CLIENTID=test-client-id
set SERVICECONTROL_AUTHENTICATION_SERVICEPULSE_AUTHORITY=https://login.microsoftonline.com/common/v2.0
set SERVICECONTROL_AUTHENTICATION_SERVICEPULSE_APISCOPES=["api://servicecontrol-test/access_as_user"]
set SERVICECONTROL_AUTHENTICATION_SERVICEPULSE_OFFLINEACCESSSCOPEENABLED=
set SERVICECONTROL_AUTHENTICATION_REQUIREHTTPSMETADATA=
set SERVICECONTROL_AUTHENTICATION_VALIDATEISSUER=
set SERVICECONTROL_AUTHENTICATION_VALIDATEAUDIENCE=
Expand Down Expand Up @@ -282,11 +283,12 @@ curl http://localhost:33633/api/authentication/configuration | json
"enabled": true,
"clientId": "test-client-id",
"audience": "api://servicecontrol-test",
"apiScopes": "[\"api://servicecontrol-test/access_as_user\"]"
"apiScopes": "[\"api://servicecontrol-test/access_as_user\"]",
"scopes": "api://servicecontrol-test/access_as_user openid profile email offline_access"
}
```

The authentication configuration endpoint is accessible without authentication and returns the configuration that clients need to authenticate. The `authority` field is omitted when `ServicePulse.Authority` is not explicitly set (it defaults to the main Authority for ServicePulse clients). The `audience` field is copied from the `ServiceControl/Authentication.Audience` value.
The authentication configuration endpoint is accessible without authentication and returns the configuration that clients need to authenticate. The `authority` field is omitted when `ServicePulse.Authority` is not explicitly set (it defaults to the main Authority for ServicePulse clients). The `audience` field is copied from the `ServiceControl/Authentication.Audience` value. The `apiScopes` field is the raw JSON array as configured. The `scopes` field is the complete, space-separated scope string ServicePulse should request, composed by ServiceControl by parsing the `apiScopes` JSON array and adding the fixed `openid profile email` scopes plus `offline_access` unless `ServiceControl/Authentication.ServicePulse.OfflineAccessScopeEnabled` is set to `false`.

### Scenario 3: Authentication with Invalid Token

Expand Down
2 changes: 1 addition & 1 deletion src/Custom.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</PropertyGroup>

<PropertyGroup>
<RuntimeFrameworkVersion>10.0.9</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion>10.0.10</RuntimeFrameworkVersion>
<RollForward>Disable</RollForward>
</PropertyGroup>

Expand Down
14 changes: 7 additions & 7 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.7.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.8.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.22.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.22.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.5" />
<PackageVersion Include="Mindscape.Raygun4Net.NetCore" Version="11.2.5" />
<PackageVersion Include="NLog.Extensions.Logging" Version="6.1.3" />
Expand Down Expand Up @@ -63,7 +63,7 @@
<PackageVersion Include="Particular.LicensingComponent.Report" Version="1.1.1" />
<PackageVersion Include="Particular.Licensing.Sources" Version="6.2.1" />
<PackageVersion Include="Particular.Obsoletes" Version="1.1.0" />
<PackageVersion Include="Particular.ServicePulse.Core" Version="2.9.1" />
<PackageVersion Include="Particular.ServicePulse.Core" Version="2.9.2" />
<PackageVersion Include="Polly.Core" Version="8.7.0" />
<PackageVersion Include="PropertyChanged.Fody" Version="4.1.0" />
<PackageVersion Include="PropertyChanging.Fody" Version="1.31.0" />
Expand All @@ -75,7 +75,7 @@
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0" />
<PackageVersion Include="System.Management" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.Management.Automation" Version="7.6.3" />
<PackageVersion Include="System.Reactive" Version="6.1.0" />
Expand All @@ -87,7 +87,7 @@
</ItemGroup>
<ItemGroup Label="Versions to pin transitive references">
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="Microsoft.IdentityModel.Abstractions" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Abstractions" Version="8.22.0" />
<PackageVersion Include="System.Formats.Cbor" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.IO.Hashing" Version="$(RuntimeFrameworkVersion)" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="$(RuntimeFrameworkVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public static async Task AssertAuthConfigurationResponse(
string expectedAuthority = null,
string expectedAudience = null,
string expectedApiScopes = null,
string expectedScopes = null,
bool expectedRoleBasedAuthorizationEnabled = false)
{
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK),
Expand Down Expand Up @@ -172,6 +173,17 @@ public static async Task AssertAuthConfigurationResponse(
$"'api_scopes' should be '{expectedApiScopes}'");
}
}

if (expectedScopes != null)
{
using (Assert.EnterMultipleScope())
{
Assert.That(root.TryGetProperty("scopes", out var scopesProperty), Is.True,
"Response should contain 'scopes' property");
Assert.That(scopesProperty.GetString(), Is.EqualTo(expectedScopes),
$"'scopes' should be '{expectedScopes}'");
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public OpenIdConnectTestConfiguration WithServicePulseClientId(string clientId)
/// Configures the API scopes that ServicePulse should request.
/// Required on the primary ServiceControl instance when authentication is enabled.
/// </summary>
/// <param name="scopes">Space-separated list of API scopes</param>
/// <param name="scopes">JSON array of API scopes (e.g. <c>["api://my-api/access_as_user"]</c>)</param>
public OpenIdConnectTestConfiguration WithServicePulseApiScopes(string scopes)
{
SetEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_APISCOPES", scopes);
Expand All @@ -159,6 +159,16 @@ public OpenIdConnectTestConfiguration WithServicePulseAuthority(string authority
return this;
}

/// <summary>
/// Configures whether ServicePulse should request the <c>offline_access</c> scope.
/// Default is true. Set to false to simulate an identity provider that disallows the scope.
/// </summary>
public OpenIdConnectTestConfiguration WithServicePulseOfflineAccessScopeEnabled(bool enabled)
{
SetEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_OFFLINEACCESSSCOPEENABLED", enabled.ToString().ToLowerInvariant());
return this;
}

/// <summary>
/// Clears all OpenID Connect environment variables.
/// Called automatically on Dispose.
Expand All @@ -176,6 +186,7 @@ public void ClearConfiguration()
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_CLIENTID");
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_APISCOPES");
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_AUTHORITY");
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_OFFLINEACCESSSCOPEENABLED");
ClearEnvironmentVariable("AUTHENTICATION_ROLEBASEDAUTHORIZATIONENABLED");
ClearEnvironmentVariable("VALIDATECONFIG");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class When_authentication_is_enabled : AcceptanceTest

const string TestAudience = "api://test-audience";
const string TestClientId = "test-client-id";
const string TestApiScopes = "api://test-audience/.default";
const string TestApiScope = "api://test-audience/.default";
// ApiScopes is configured as a JSON array (the format ServicePulse parses)
const string TestApiScopes = "[\"api://test-audience/.default\"]";

[SetUp]
public void ConfigureAuth()
Expand Down Expand Up @@ -75,6 +77,7 @@ await OpenIdConnectAssertions.AssertAuthConfigurationResponse(
expectedClientId: TestClientId,
expectedAudience: TestAudience,
expectedApiScopes: TestApiScopes,
expectedScopes: $"{TestApiScope} openid profile email offline_access",
expectedRoleBasedAuthorizationEnabled: true);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace ServiceControl.AcceptanceTests.Security.OpenIdConnect
{
using System.Net.Http;
using System.Threading.Tasks;
using AcceptanceTesting;
using AcceptanceTesting.OpenIdConnect;
using NServiceBus.AcceptanceTesting;
using NUnit.Framework;

/// <summary>
/// ServicePulse Offline Access Scope Opt-Out
/// When Authentication.ServicePulse.OfflineAccessScopeEnabled is false, the composed scope
/// string returned by the authentication configuration endpoint should omit offline_access,
/// so ServicePulse does not request a scope the identity provider disallows.
/// </summary>
class When_service_pulse_offline_access_scope_is_disabled : AcceptanceTest
{
OpenIdConnectTestConfiguration configuration;

const string TestAuthority = "https://login.example.com/tenant-id/v2.0";
const string TestAudience = "api://test-audience";
const string TestClientId = "test-client-id";
const string TestApiScope = "api://test-audience/.default";
// ApiScopes is configured as a JSON array (the format ServicePulse parses)
const string TestApiScopes = "[\"api://test-audience/.default\"]";

[SetUp]
public void ConfigureAuth() =>
configuration = new OpenIdConnectTestConfiguration(ServiceControlInstanceType.Primary)
.WithConfigurationValidationDisabled()
.WithAuthenticationEnabled()
.WithAuthority(TestAuthority)
.WithAudience(TestAudience)
.WithServicePulseClientId(TestClientId)
.WithServicePulseApiScopes(TestApiScopes)
.WithServicePulseOfflineAccessScopeEnabled(false)
.WithRequireHttpsMetadata(false);

[TearDown]
public void CleanupAuth() => configuration?.Dispose();

[Test]
public async Task Should_omit_offline_access_from_composed_scopes()
{
HttpResponseMessage response = null;

_ = await Define<Context>()
.Done(async ctx =>
{
response = await OpenIdConnectAssertions.SendRequestWithoutAuth(
HttpClient,
HttpMethod.Get,
"/api/authentication/configuration");
return response != null;
})
.Run();

await OpenIdConnectAssertions.AssertAuthConfigurationResponse(
response,
expectedEnabled: true,
expectedClientId: TestClientId,
expectedAudience: TestAudience,
expectedApiScopes: TestApiScopes,
expectedScopes: $"{TestApiScope} openid profile email");
}

class Context : ScenarioContext;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"ServicePulseAuthority": null,
"ServicePulseClientId": null,
"ServicePulseApiScopes": null,
"ServicePulseOfflineAccessScopeEnabled": true,
"ServicePulseScopes": null,
"RolesClaim": "roles",
"RoleBasedAuthorizationEnabled": false
},
Expand Down
Loading