Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(oidc): OIDC/SSO relying-party helpers (CONTRACT §12) - #24

#24
Merged
ilpanich merged 2 commits into
mainilpanich/axiam-csharp-sdk:mainfrom
claude/sdk-oidc-sso-plan-8t4hgmilpanich/axiam-csharp-sdk:claude/sdk-oidc-sso-plan-8t4hgmCopy head branch name to clipboard
Jul 27, 2026
Merged

feat(oidc): OIDC/SSO relying-party helpers (CONTRACT §12)#24
ilpanich merged 2 commits into
mainilpanich/axiam-csharp-sdk:mainfrom
claude/sdk-oidc-sso-plan-8t4hgmilpanich/axiam-csharp-sdk:claude/sdk-oidc-sso-plan-8t4hgmCopy head branch name to clipboard

Conversation

@ilpanich

Copy link
Copy Markdown
Owner

Why

The SDK could validate inbound tokens but could not drive the OIDC/SSO flow — no discovery, no PKCE authorization-URL construction, no code exchange, no relying-party ID-token validation, no client_credentials grant. A developer could not implement "Login with AXIAM".

Implements CONTRACT §12 (contract 1.5), ported from the TypeScript reference implementation.

What

All nine §12 operations on AxiamClient (via a new partial class): OidcDiscoverAsync, OidcBegin (no Async suffix — it performs no network I/O, which §12.2 fixes as a deliberate exception and verifies here with a reflection test), OidcExchangeAsync, OidcRefreshAsync, LoginClientCredentialsAsync, IntrospectAsync, RevokeAsync, SsoStartAsync, SsoCompleteAsync.

  • Axiam.Sdk/Auth/Oidc/ — types, wire shapes, S256-only PKCE on RandomNumberGenerator + SHA256, the seven §12.4 validation rules with the contract's exact reason codes, and IOidcStateStore + MemoryOidcStateStore (10-min TTL, single-use consume, lazy sweep).
  • Axiam.Sdk/AxiamClient.Oidc.cs — the operations, with an origin-keyed single-flight discovery cache.
  • Axiam.Sdk.AspNetCore/OidcLoginEndpoints.csMapAxiamOidcLogin wiring the login-redirect and callback endpoints into the existing pipeline, with IOidcStateStore registered via DI.
  • Core/OAuthProtocolError.cs — subclasses AuthError (which is no longer sealed), message exactly "<error>: <error_description>", so existing catch (AuthError) sites keep matching; verified by a dedicated test.
  • Rest/AxiamHttpMessageHandler.cs/oauth2/* exempted from the §9 reactive-refresh path, so a 401 there never enters the guard.
  • Auth/JwksVerifier.cs — extended with VerifyOidcIdTokenSignatureAsync / ForJwksUri rather than forked.

Tokens, client_secret, and code_verifier are Sensitive-wrapped; state and nonce are plain strings (not secrets).

Tests

328 tests across the solution (291 core + 37 ASP.NET Core), all passing. Merged line coverage 96.05% against the 94% floor, measured with coverlet using the same lcov merge the coverage workflow performs.

Includes the RFC 7636 Appendix B PKCE vector, one test per §12.4 failure mode (with alg: none, missing kid, unknown kid after one re-fetch, missing/multi-aud without azp, missing exp/iat, future iat/nbf, missing and wrong nonce), an all-or-nothing discard test, discovery cache/TTL/single-flight under concurrent tasks, OAuth2ErrorResponseOAuthProtocolError, a 401 from IntrospectAsync/RevokeAsync not entering the §9 guard, revoke idempotency, state-store concurrency, redaction in ToString(), and real TestServer integration tests for MapAxiamOidcLogin.

Verified locally: dotnet build Debug and Release, both example projects, dotnet test --no-build (328/328), the coverage merge and floor check, dotnet list package --vulnerable --include-transitive (none), the exact TLS-bypass grep gate from CI, and dotnet pack for both packages.

Notes

  • Sensitive<T> gains a public Expose() and a public Wrap(T) factory. §12 returns tokens in the /oauth2/token response body rather than a cookie the SDK captures, so a caller must be able to read them; contract §7 was restructured in 1.5 to permit exactly one explicit accessor for this reason. ToString() and JSON serialization still always redact, and no implicit conversion or public field was added.
  • OidcRefreshAsync uses a dedicated SemaphoreSlim(1,1) + Task<T> guard rather than the cookie-session RefreshGuard, whose TokenPair-freshness comparison is meaningless for an independent OAuth2 token stream. Permitted by contract 1.5 §9 rule 5; the observable one-in-flight-with-shared-result requirement is met.
  • LoginClientCredentialsParams.AdoptAsCredential is deliberately not implemented (it is a contract MAY) and throws NotSupportedException rather than silently ignoring the flag.
  • Depends on the contract PR in ilpanich/axiam (vendored CONTRACT.md re-synced to 1.5 here).

Generated by Claude Code

claude added 2 commits July 27, 2026 12:43
Implements the nine canonical §12 operations directly on AxiamClient
(OidcDiscoverAsync, OidcBegin, OidcExchangeAsync, OidcRefreshAsync,
LoginClientCredentialsAsync, IntrospectAsync, RevokeAsync, SsoStartAsync,
SsoCompleteAsync), full §12.4 ID-token validation reusing the existing
JwksVerifier (extended, not forked), an IOidcStateStore/MemoryOidcStateStore
pair, and Axiam.Sdk.AspNetCore's MapAxiamOidcLogin minimal-API glue.

OAuthProtocolError is added as a sub-type of AuthError (which is no longer
sealed) so existing catch (AuthError) call sites keep working; ID-token
failures ride the same AuthError type via a new optional Reason property.
Sensitive<T> gains a public Expose()/Wrap() pair, resolving the documented
§7-vs-§12 tension: §12 must hand raw tokens back to the caller, unlike the
cookie-only §1-§11 surface.

No new runtime dependency (BCL crypto only). Re-syncs vendored CONTRACT.md
and updates README/CHANGELOG/index.md/examples accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contract 1.5 is the non-breaking, clarifying amendment produced by the
cross-SDK §12 conformance review: §7 restructured into four numbered rules
separating the unconditional redaction MUST from the explicit-accessor MAY
(which is what this SDK's new public Sensitive<T>.Expose() relies on — its XML
doc comment already anticipated this amendment), §9 rule 5 (mechanism is free;
a dedicated guard instance is permitted, as this SDK's SemaphoreSlim-plus-cached-Task
guard does) plus an explicit statement of §9 rule 2's observable requirement,
and ten §12 corrections: the self-contradictory §12.3 rule 6 cache-keying text
(this SDK's per-instance cache is now explicitly conformant), §12.4 rule 2's
unimplementable literal "one re-fetch", the closed seven-code ID-token reason
vocabulary, the slug-header/UUID-query asymmetry, client_id removed from
oidc_begin's per-call inputs, AuthorizationRequest's absent redirect_uri
documented as caller-owned, revoke's "any 2xx" vs the §2 5xx row, and one
normative sentence permitting either host object for the nine methods.

Also fix the conformance checklist heading, which still read "§1–§11" under a
statement claiming §1–§12 even though the table below it already carried a §12
row.

Source of truth: ilpanich/axiam sdks/CONTRACT.md.
@ilpanich
ilpanich merged commit 400a750 into main Jul 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.