Skip to content

Commit 03227dc

Browse files
committed
address copilot comments
1 parent 3f0cca7 commit 03227dc

6 files changed

Lines changed: 73 additions & 33 deletions

File tree

samples/dotnet/saml-sp-login/tests/AuthIntegrationTests.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,59 @@
99

1010
namespace SamlSpLogin.Tests;
1111

12-
public class AuthIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
12+
public class AuthIntegrationTests : IClassFixture<WebApplicationFactory<Program>>, IDisposable
1313
{
14+
private static readonly string[] ManagedEnvVars =
15+
{
16+
"SAML_SP_ENTITY_ID",
17+
"SAML_IDP_ENTITY_ID",
18+
"SAML_IDP_SSO_URL",
19+
"SAML_IDP_SIGNING_CERT_PATH",
20+
};
21+
1422
private readonly WebApplicationFactory<Program> _factory;
23+
private readonly string _certPath;
24+
private readonly Dictionary<string, string?> _previousEnv;
1525

1626
public AuthIntegrationTests(WebApplicationFactory<Program> factory)
1727
{
1828
// Write a self-signed stub PEM cert to a temp file so the SP startup code
1929
// (which loads SAML_IDP_SIGNING_CERT_PATH at boot) finds a valid X.509.
2030
// The cert is never validated against a real assertion; tests stub auth
2131
// entirely via TestAuthHandler.
22-
var certPath = WriteStubCertPemFile();
32+
_certPath = WriteStubCertPemFile();
33+
34+
// Snapshot prior env-var values so Dispose can restore them, avoiding
35+
// process-wide leakage between test classes or runs.
36+
_previousEnv = ManagedEnvVars.ToDictionary(
37+
name => name,
38+
name => Environment.GetEnvironmentVariable(name));
2339

2440
Environment.SetEnvironmentVariable("SAML_SP_ENTITY_ID", "https://localhost:4262/saml/metadata");
2541
Environment.SetEnvironmentVariable("SAML_IDP_ENTITY_ID", "https://idp.example/test");
2642
Environment.SetEnvironmentVariable("SAML_IDP_SSO_URL", "https://idp.example/test/sso");
27-
Environment.SetEnvironmentVariable("SAML_IDP_SIGNING_CERT_PATH", certPath);
43+
Environment.SetEnvironmentVariable("SAML_IDP_SIGNING_CERT_PATH", _certPath);
2844

2945
_factory = factory;
3046
}
3147

48+
public void Dispose()
49+
{
50+
foreach (var name in ManagedEnvVars)
51+
{
52+
Environment.SetEnvironmentVariable(name, _previousEnv[name]);
53+
}
54+
try
55+
{
56+
if (File.Exists(_certPath)) File.Delete(_certPath);
57+
}
58+
catch
59+
{
60+
// Best-effort cleanup; ignore any I/O failures.
61+
}
62+
GC.SuppressFinalize(this);
63+
}
64+
3265
[Fact]
3366
public async Task Root_Unauthenticated_ShowsSignIn()
3467
{

samples/java/saml-sp-login/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ The next SAML response will include an `<AttributeStatement>` with those attribu
5151
- Cross-site cookie config (`SameSite=None; Secure`) so the JSESSIONID survives the POST from CIAM back to the ACS endpoint
5252
- A targeted bearer-confirmation validator that works around CIAM-specific quirks: a malformed `Address` attribute (host:port form instead of IP-only) and the unreliability of session-stored `InResponseTo` state across cross-site POSTs. The response-level signature (verified against the IdP signing cert) still runs and provides the core security guarantee.
5353
- An ACS endpoint at `/login/saml2/sso/secureauth` that validates IdP-signed SAML responses and accepts both SP- and IdP-initiated flows
54-
- Customizing `OpenSaml5AuthenticationProvider`'s response validator to accept unsolicited (IdP-initiated) responses
55-
- Server-managed session via Spring Session carrying the SAML `Saml2AuthenticatedPrincipal`
54+
- Customizing `OpenSaml4AuthenticationProvider`'s response validator to accept unsolicited (IdP-initiated) responses
55+
- Server-managed session via the default servlet `HttpSession` / `JSESSIONID` carrying the SAML `Saml2AuthenticatedPrincipal`
5656
- Local logout (clears the session — CIAM does not implement SAML SLO)
5757

5858
## Tests

scripts/aggregate-manifests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ async function main() {
8383
run_command: _rc,
8484
lib: _lib,
8585
docs_url: _docs,
86+
callout: _callout,
8687
...scenarioForAllFrameworks
8788
} = scenario;
8889
scenarios[scenarioId] = {

scripts/extract-snippets.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ interface FrameworkSnippet {
3131
install: string;
3232
repo_path: string;
3333
run_command?: string;
34+
callout?: string;
3435
}
3536

3637
interface ScenarioMeta {
3738
run_command?: string;
3839
lib?: string;
3940
docs_url?: string;
41+
callout?: string;
4042
[key: string]: unknown;
4143
}
4244

@@ -315,6 +317,7 @@ async function main() {
315317
const scenarioLib = manifest.scenarios[scenarioId]?.lib ?? manifest.lib;
316318
const scenarioDocsUrl =
317319
manifest.scenarios[scenarioId]?.docs_url ?? manifest.docs_url;
320+
const scenarioCallout = manifest.scenarios[scenarioId]?.callout;
318321
snippets[scenarioId][fw] = {
319322
steps: allSteps,
320323
framework: fw,
@@ -324,6 +327,7 @@ async function main() {
324327
install: getInstallCommand(scenarioDir),
325328
repo_path: `samples/${path.relative(SAMPLES, scenarioDir)}`,
326329
...(runCommand ? { run_command: runCommand } : {}),
330+
...(scenarioCallout ? { callout: scenarioCallout } : {}),
327331
};
328332
}
329333
}

snippet-manifest.yaml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ scenarios:
4242
display_name: User Login
4343
grant: Auth Code + PKCE
4444
description: Redirect users to SecureAuth for login. PKCE protects against code interception.
45-
callout: Uses PKCE — no client secret needed. The browser generates a code_verifier/code_challenge pair.
4645
config_rows:
4746
- label: Client ID
4847
value: "{{CLIENT_ID}}"
@@ -65,9 +64,6 @@ scenarios:
6564
display_name: Token Refresh
6665
grant: Refresh Token
6766
description: Use refresh tokens to get new access tokens without re-prompting the user. Requires offline_access scope.
68-
callout: >-
69-
Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the
70-
application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope.
7167
required_scopes:
7268
- offline_access
7369
config_rows:
@@ -92,9 +88,6 @@ scenarios:
9288
description: >-
9389
Redirect users to SecureAuth for login. The server holds the client secret and manages the session; PKCE adds
9490
defense in depth.
95-
callout: >-
96-
Server-side flow — the client_secret never reaches the browser. PKCE is still enabled to protect against
97-
authorization-code interception during the redirect back to /callback.
9891
config_rows:
9992
- label: Client ID
10093
value: "{{CLIENT_ID}}"
@@ -121,9 +114,6 @@ scenarios:
121114
description: >-
122115
Use refresh tokens server-side to renew the user's access token without re-prompting them. Requires the
123116
offline_access scope.
124-
callout: >-
125-
Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the
126-
application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope.
127117
required_scopes:
128118
- offline_access
129119
config_rows:
@@ -150,9 +140,6 @@ scenarios:
150140
display_name: SAML Login
151141
grant: SAML 2.0
152142
description: SAML SSO with SecureAuth as the IdP. The same ACS endpoint accepts SP-initiated and IdP-initiated responses.
153-
callout: >-
154-
AuthnRequests are unsigned for simplicity — CIAM accepts unsigned requests by default. For production, configure
155-
SP request signing per your library's docs.
156143
config_rows:
157144
- label: SP Entity ID
158145
value: "{{SAML_SP_ENTITY_ID}}"

snippets.json

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"docs_url": "https://github.com/damienbod/angular-auth-oidc-client",
3434
"install": "angular-auth-oidc-client",
3535
"repo_path": "samples/angular/login-pkce",
36-
"run_command": "yarn install && yarn start"
36+
"run_command": "yarn install && yarn start",
37+
"callout": "Uses PKCE — no client secret needed. The browser generates a code_verifier/code_challenge pair."
3738
},
3839
"react": {
3940
"steps": [
@@ -76,7 +77,8 @@
7677
"docs_url": "https://github.com/authts/react-oidc-context",
7778
"install": "oidc-client-ts react-oidc-context",
7879
"repo_path": "samples/react/login-pkce",
79-
"run_command": "yarn install && yarn start"
80+
"run_command": "yarn install && yarn start",
81+
"callout": "Uses PKCE — no client secret needed. The browser generates a code_verifier/code_challenge pair."
8082
},
8183
"vue": {
8284
"steps": [
@@ -119,7 +121,8 @@
119121
"docs_url": "https://github.com/authts/oidc-client-ts",
120122
"install": "oidc-client-ts",
121123
"repo_path": "samples/vue/login-pkce",
122-
"run_command": "yarn install && yarn start"
124+
"run_command": "yarn install && yarn start",
125+
"callout": "Uses PKCE — no client secret needed. The browser generates a code_verifier/code_challenge pair."
123126
}
124127
},
125128
"spa_token_refresh": {
@@ -156,7 +159,8 @@
156159
"docs_url": "https://github.com/damienbod/angular-auth-oidc-client",
157160
"install": "angular-auth-oidc-client",
158161
"repo_path": "samples/angular/token-refresh",
159-
"run_command": "yarn install && yarn start"
162+
"run_command": "yarn install && yarn start",
163+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
160164
},
161165
"react": {
162166
"steps": [
@@ -199,7 +203,8 @@
199203
"docs_url": "https://github.com/authts/react-oidc-context",
200204
"install": "oidc-client-ts react-oidc-context",
201205
"repo_path": "samples/react/token-refresh",
202-
"run_command": "yarn install && yarn start"
206+
"run_command": "yarn install && yarn start",
207+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
203208
},
204209
"vue": {
205210
"steps": [
@@ -242,7 +247,8 @@
242247
"docs_url": "https://github.com/authts/oidc-client-ts",
243248
"install": "oidc-client-ts",
244249
"repo_path": "samples/vue/token-refresh",
245-
"run_command": "yarn install && yarn start"
250+
"run_command": "yarn install && yarn start",
251+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
246252
}
247253
},
248254
"server_login_auth_code": {
@@ -279,7 +285,8 @@
279285
"docs_url": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication",
280286
"install": "",
281287
"repo_path": "samples/dotnet/login-auth-code",
282-
"run_command": "dotnet restore && dotnet run --project src"
288+
"run_command": "dotnet restore && dotnet run --project src",
289+
"callout": "Server-side flow — the client_secret never reaches the browser. PKCE is still enabled to protect against authorization-code interception during the redirect back to /callback."
283290
},
284291
"java": {
285292
"steps": [
@@ -322,7 +329,8 @@
322329
"docs_url": "https://docs.spring.io/spring-security/reference/servlet/oauth2/login/index.html",
323330
"install": "",
324331
"repo_path": "samples/java/login-auth-code",
325-
"run_command": "mvn spring-boot:run"
332+
"run_command": "mvn spring-boot:run",
333+
"callout": "Server-side flow — the client_secret never reaches the browser. PKCE is still enabled to protect against authorization-code interception during the redirect back to /login/oauth2/code/secureauth."
326334
},
327335
"node": {
328336
"steps": [
@@ -365,7 +373,8 @@
365373
"docs_url": "https://github.com/panva/openid-client",
366374
"install": "openid-client",
367375
"repo_path": "samples/node/login-auth-code",
368-
"run_command": "yarn install && yarn start"
376+
"run_command": "yarn install && yarn start",
377+
"callout": "Server-side flow — the client_secret never reaches the browser. PKCE is still enabled to protect against authorization-code interception during the redirect back to /callback."
369378
}
370379
},
371380
"server_token_refresh": {
@@ -418,7 +427,8 @@
418427
"docs_url": "https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-oidc-web-authentication",
419428
"install": "",
420429
"repo_path": "samples/dotnet/token-refresh",
421-
"run_command": "dotnet restore && dotnet run --project src"
430+
"run_command": "dotnet restore && dotnet run --project src",
431+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
422432
},
423433
"java": {
424434
"steps": [
@@ -477,7 +487,8 @@
477487
"docs_url": "https://docs.spring.io/spring-security/reference/servlet/oauth2/login/index.html",
478488
"install": "",
479489
"repo_path": "samples/java/token-refresh",
480-
"run_command": "mvn spring-boot:run"
490+
"run_command": "mvn spring-boot:run",
491+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
481492
},
482493
"node": {
483494
"steps": [
@@ -528,7 +539,8 @@
528539
"docs_url": "https://github.com/panva/openid-client",
529540
"install": "openid-client",
530541
"repo_path": "samples/node/token-refresh",
531-
"run_command": "yarn install && yarn start"
542+
"run_command": "yarn install && yarn start",
543+
"callout": "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
532544
}
533545
},
534546
"server_saml_sp_login": {
@@ -565,7 +577,8 @@
565577
"docs_url": "https://github.com/Sustainsys/Saml2",
566578
"install": "",
567579
"repo_path": "samples/dotnet/saml-sp-login",
568-
"run_command": "dotnet restore && dotnet run --project src"
580+
"run_command": "dotnet restore && dotnet run --project src",
581+
"callout": "AuthnRequests are unsigned for simplicity — CIAM accepts unsigned requests by default. For production, configure SP request signing per your library's docs."
569582
},
570583
"java": {
571584
"steps": [
@@ -600,7 +613,8 @@
600613
"docs_url": "https://docs.spring.io/spring-security/reference/servlet/saml2/login/index.html",
601614
"install": "",
602615
"repo_path": "samples/java/saml-sp-login",
603-
"run_command": "mvn spring-boot:run"
616+
"run_command": "mvn spring-boot:run",
617+
"callout": "Spring Security 6.4 always signs AuthnRequests; this sample reuses the dev TLS keystore as the SP signing credential. CIAM doesn't validate the signature (no SP cert registered), so the request is accepted as if unsigned. For production, generate a dedicated SP signing key and register its public cert in CIAM's SAML SP form."
604618
},
605619
"node": {
606620
"steps": [
@@ -635,7 +649,8 @@
635649
"docs_url": "https://github.com/node-saml/passport-saml",
636650
"install": "@node-saml/passport-saml passport",
637651
"repo_path": "samples/node/saml-sp-login",
638-
"run_command": "yarn install && yarn start"
652+
"run_command": "yarn install && yarn start",
653+
"callout": "AuthnRequests are unsigned for simplicity — CIAM accepts unsigned requests by default. For production, configure SP request signing per your library's docs."
639654
}
640655
}
641656
}

0 commit comments

Comments
 (0)