feat(oidc): OIDC/SSO relying-party helpers (CONTRACT §12) - #24
#24Merged
ilpanich merged 2 commits intoJul 27, 2026
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
Merged
feat(oidc): OIDC/SSO relying-party helpers (CONTRACT §12)#24ilpanich merged 2 commits intomainilpanich/axiam-csharp-sdk:mainfrom claude/sdk-oidc-sso-plan-8t4hgmilpanich/axiam-csharp-sdk:claude/sdk-oidc-sso-plan-8t4hgmCopy head branch name to clipboard
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_credentialsgrant. 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(noAsyncsuffix — 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 onRandomNumberGenerator+SHA256, the seven §12.4 validation rules with the contract's exact reason codes, andIOidcStateStore+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.cs—MapAxiamOidcLoginwiring the login-redirect and callback endpoints into the existing pipeline, withIOidcStateStoreregistered via DI.Core/OAuthProtocolError.cs— subclassesAuthError(which is no longersealed), message exactly"<error>: <error_description>", so existingcatch (AuthError)sites keep matching; verified by a dedicated test.Rest/AxiamHttpMessageHandler.cs—/oauth2/*exempted from the §9 reactive-refresh path, so a401there never enters the guard.Auth/JwksVerifier.cs— extended withVerifyOidcIdTokenSignatureAsync/ForJwksUrirather than forked.Tokens,
client_secret, andcode_verifierareSensitive-wrapped;stateandnonceare 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, missingkid, unknownkidafter one re-fetch, missing/multi-audwithoutazp, missingexp/iat, futureiat/nbf, missing and wrongnonce), an all-or-nothing discard test, discovery cache/TTL/single-flight under concurrent tasks,OAuth2ErrorResponse→OAuthProtocolError, a401fromIntrospectAsync/RevokeAsyncnot entering the §9 guard, revoke idempotency, state-store concurrency, redaction inToString(), and realTestServerintegration tests forMapAxiamOidcLogin.Verified locally:
dotnet buildDebug 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, anddotnet packfor both packages.Notes
Sensitive<T>gains a publicExpose()and a publicWrap(T)factory. §12 returns tokens in the/oauth2/tokenresponse 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.OidcRefreshAsyncuses a dedicatedSemaphoreSlim(1,1)+Task<T>guard rather than the cookie-sessionRefreshGuard, whoseTokenPair-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.AdoptAsCredentialis deliberately not implemented (it is a contract MAY) and throwsNotSupportedExceptionrather than silently ignoring the flag.ilpanich/axiam(vendoredCONTRACT.mdre-synced to 1.5 here).Generated by Claude Code