This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
github.com/hyperscale-stack/security is a transport-agnostic authentication
and authorization framework for Go — conceptually Spring Security / Symfony
Security for the Go ecosystem. It is a Go workspace (go.work) split into
several independently-releasable modules, so consumers import only the
pieces they need and the core stays free of heavy transitive dependencies.
The whole v0.x series was replaced by a ground-up transport-agnostic
rewrite; the legacy authentication/ / authorization/ packages were
removed. The rewrite is functionally complete and currently [Unreleased]:
its first release is v1.0.0 for every module (the module paths carry
no /vN suffix; see docs/releasing.md). Remaining gaps are tracked as
GitHub issues, not refactor phases. Source-of-truth docs:
CHANGELOG.md— what the first release ships.MIGRATION.md— workspace layout and dependency policy.LIMITATIONS.md— known gaps and explicitly out-of-scope items.docs/—architecture.md,oidc-provider.md,oidc-client.md,oidc-conformance.md,saml-sp.md,observability.md,security-considerations.md,migration-from-v0.md,releasing.md.
These rules are mandatory when working in this repository:
- Clean code: write clean, SOLID, testable code. No overengineering — build for the current requirement, not a hypothetical one.
- Language: all code, commit messages, and documentation are in English. Talk to the user (Axel) in French.
- Tests are mandatory: always write tests. Target 100% coverage where practical, never below 80%.
- RFC compliance: anything based on an RFC (OAuth2, JWT, PKCE, ...) MUST follow the RFC to the letter.
- Security: write secure code with no known vulnerabilities.
gosecis part of the lint gate — keep it clean. - Per-step gate: at the end of every feature/step, run
make test lint check-modules(andmake vulnbefore opening a pull request). They MUST pass.make generateruns on its own throughtest,lint,vulnandtidy. - Commit what
make tidy/go work syncchange. CI fails when ago.modorgo.sumis out of sync with the workspace or untidy on its own (make check-modules). To resolve ago.modconflict: take either side, runmake tidy, thenmake check-modules, and commit. - Commit per step: once tests and lint pass, commit the step before
moving on. Use Conventional Commits with a module scope, as in the git
history (
feat(oauth2): ...,fix(jwt): ...,docs(...): ...). - Godoc: document every public API with godoc. Keep it concise and relevant so a developer can pick up the module quickly.
All targets operate on every module in the workspace (discovered via
find . -name go.mod). Run from the repo root:
make build # go build ./... in every module
make generate # mockery mocks (in-package mock_*_test.go, never committed), see "Tooling caveats"
make test # generate, then go test -race -cover in every module, aggregated -> build/coverage.out
make lint # golangci-lint (pinned in tools/golangci-lint, shared .golangci.yml) on every module
make vuln # govulncheck -test on every module, outside the workspace
make test-backends # the store suites on real PostgreSQL/MySQL/MariaDB/Redis (variables: scripts/test-backends.sh); CI runs it
make tools # build the pinned tools (tools/<name>) into build/bin
make tidy # go work sync + go mod tidy per module (and tools/*) + go work sync
make check-modules # one go line, go.work complete, sync no-op, every module tidy/buildable with GOWORK=off
make release-check TAG=oauth2/v1.0.0 # pre-push check of a release tag (docs/releasing.md)
make sync # go work sync
make bench # benchmarks across all modules
make coverage # go tool cover -func on the aggregated profile
make e2e # opt-in browser suites (Tales + Chrome: e2e/saml, e2e/oidc), never in CI
make e2e-oidc # only the OIDC one (make e2e-saml: only the SAML one)
make conformance # opt-in: the OpenID Foundation suite against oidc/provider and oidc/client (Docker, OIDF_SUITE_DIR)To run a single test or work on one module, cd into that module first
(each module is its own go.mod with replace directives back to the core).
Run make generate once before running go test directly: the tests use
the generated mocks.
make generate
cd oauth2 && go test -race -run TestServer_Token ./...
cd jwt && go test ./...make test aggregates coverage but excludes example main() programs
(they bind a socket and block); examples are still built, tested, and linted.
CI (read-only token by default, only the release publish job gets
contents: write; every action pinned to a commit SHA; credentials never
persisted; keep it actionlint- and zizmor-clean):
.github/workflows/go.ymlrunsmake tools generate check-modules build test lintagainst the whole workspace, thenmake coveralls(goveralls, pinned intools/goveralls; a Coveralls outage does not fail the build); a second job,Store suites (real backends), runsmake test-backendsagainst PostgreSQL 16, MySQL 8.0/8.4, MariaDB 11.4 and Redis 7.4 service containers (digests of the compose files, loopback only, no credentials). A real-backend test skips while its variable is unset, and that run fails on any skip: a new real-backend test follows that rule, its variable is added toscripts/test-backends.shand to the job, and a package outside the suites the script lists gets asuiteline there (otherwise it never runs in CI and the job stays green);govulncheck.ymlrunsmake vuln(every module, tests included) on every change and weekly;release.yml, on av*or**/v*tag: a read-onlyverifyjob (scripts/release-check.sh,check-modules, build, test, lint, and govulncheck on the released module), then apublishjob (contents: write, environmentrelease,gh release create, no checkout). Seedocs/releasing.md.
| Path | Import / pkg name | Purpose |
|---|---|---|
. |
security |
Core transport-agnostic primitives |
./outbound |
.../outbound |
SSRF-safe outbound *http.Client — package of the core |
./http |
.../http → httpsec |
net/http adapter (middleware, Authorize, carrier) |
./grpc |
.../grpc → grpcsec |
gRPC unary/stream interceptors + Authorize |
./connectrpc |
.../connectrpc → connectrpcsec |
ConnectRPC auth + authorize interceptors |
./basic |
.../basic |
HTTP Basic extractor + authenticator |
./bearer |
.../bearer |
Bearer extractor + TokenVerifier authenticator |
./password |
.../password |
BCrypt + Argon2id hashers (NeedsRehash) |
./jwt |
.../jwt → jwtsec |
JWT signer/verifier + JWKS + key rotation |
./session |
.../session |
Stateless AES-256-GCM cookie sessions + CSRF |
./oauth2 |
.../oauth2 |
OAuth2 authorization server |
./oauth2/storage/memory |
.../oauth2/storage/memory |
In-memory oauth2.Storage — package of oauth2 |
./oauth2/store/sql |
.../oauth2/store/sql |
Production storage on database/sql (PG/MySQL/SQLite) |
./oauth2/store/redis |
.../oauth2/store/redis |
Production storage on Redis (Lua atomicity) |
./oidc/client |
.../oidc/client → oidcclient |
OpenID Connect Relying Party (code flow + PKCE) |
./oidc/client/storage/memory |
.../oidc/client/storage/memory |
In-memory TransactionStore — package of oidc/client |
./oidc/client/storetest |
.../oidc/client/storetest |
Store conformance suite — package of oidc/client |
./oidc/client/store/sql |
.../oidc/client/store/sql → sqlstore |
Transaction store on database/sql (PG/MySQL/SQLite) |
./oidc/client/store/redis |
.../oidc/client/store/redis → redisstore |
Transaction store on Redis (Lua atomicity) |
./oidc/provider |
.../oidc/provider → oidcprovider |
OpenID Provider on the oauth2 extension points |
./saml/sp |
.../saml/sp → samlsp |
SAML 2.0 Service Provider (SP-initiated SSO) |
./saml/sp/storage/memory |
.../saml/sp/storage/memory |
In-memory samlsp.Storage — package of saml/sp |
./saml/sp/storetest |
.../saml/sp/storetest |
Storage conformance suite — package of saml/sp |
./saml/sp/store/sql |
.../saml/sp/store/sql → sqlstore |
SAML storage on database/sql (PG/MySQL/SQLite) |
./saml/sp/store/redis |
.../saml/sp/store/redis → redisstore |
SAML storage on Redis (single-key Lua scripts) |
./examples |
.../examples |
Runnable demos: basic-http, bearer-jwt, grpc-bearer, connectrpc-bearer, session-web, oauth2, oidc-client, oidc-provider, saml-sp |
./internal/integrations |
(private) | Cross-module end-to-end tests |
./internal/interop/oidcop |
(private, tests only) | oidc/provider vs go-oidc + x/oauth2 as independent RP |
./internal/interop/oidcrp |
(private, tests only) | oidc/client against zitadel/oidc + opt-in Chrome test |
./internal/interop/oidcstack |
(private, tests only) | our RP ↔ our OP end to end, attacks included; real-backend store runs (CI job; opt-in locally) |
./internal/interop/saml |
(private, tests only) | saml/sp against crewjam's IdP, Keycloak (pinned + opt-in live), shared stores, e2e helper |
./internal/interop/sqlstores |
(private, tests only) | the three SQL stores on real servers (SQLite always; PostgreSQL, MySQL, MariaDB in the CI job, opt-in locally): racing Migrate, oauth2 conformance, MySQL upgrade statements |
oauth2/storage/memory is not a standalone module — it is a sub-package
of oauth2; likewise oidc/client/storage/memory and oidc/client/storetest
belong to oidc/client, and saml/sp/storage/memory and saml/sp/storetest
to saml/sp; outbound is a sub-package of the core (stdlib only, the core's
only networking code: every server-side fetch of a configuration- or
metadata-supplied URL MUST go through outbound.NewClient). The other rows
are independent modules (own go.mod).
The dependency direction is a hard rule (enforced by review, see
MIGRATION.md): the core (.) must depend only on stdlib +
go.opentelemetry.io/otel (+ testify in its own tests). It MUST NOT
import gRPC, ConnectRPC, JOSE/JWT libs, OAuth2, Redis, SQL drivers, HTTP
routers, or concrete loggers. Adapters depend on the core, never the reverse. The
oauth2 module has no hard dependency on jwt — JWT access tokens are
wired via an adapter (jwt depends on oauth2, not the other way).
oidc/client depends on the core and go-jose only — neither oauth2 nor
jwt; coreos/go-oidc and golang.org/x/oauth2 are not dependencies (see
the evaluation in oidc/client/doc.go). oidc/provider depends on core +
oauth2 + jwt only (go-jose stays behind jwt; TestDependencyPolicy
enforces it); go-oidc and x/oauth2 are allowed only in the test-only
internal/interop/oidcop module, as an independent relying party. The
SAML XML-DSig stack (gosaml2, goxmldsig ≥ v1.6.1, etree) stays inside
saml/sp; crewjam/saml, the SQL drivers (pgx, go-sql-driver/mysql,
modernc sqlite), go-redis and miniredis are test-only dependencies of
internal/interop/saml. When adding code, check the allowed-dependency
list in MIGRATION.md before adding an import.
Every sub-module declares replace github.com/hyperscale-stack/security => ../
(=> ../../ for oidc/provider and internal/integrations, => ../../../
for the OAuth2 SQL/Redis stores and the interop modules, => ../../../../
for the OIDC client and SAML SP stores, which also replace
oidc/client => ../../ or saml/sp => ../../) plus
one per in-repo module of its graph, so local dev works without published
versions; consumers ignore them, so a release commit sets the in-repo
requirements to real versions (docs/releasing.md). New modules declare
go 1.27.2: every go.mod and go.work carry the same go line, and a
new module must be added to go.work (make check-modules enforces both).
A request flows through this pipeline (all of it transport-agnostic — HTTP and gRPC are just adapters):
Carrier ──> Extractor ──> Authentication ──> Manager/Authenticator ──> Engine
│
AccessDecisionManager/Voter <──┘
Carrier— abstracts a transport message (HTTP request, gRPC metadata) withhttp.Header-like Get/Set/Add/Values. Adapters wrap it. Get/Values read header fields only — never the query string or cookies; the session module reads its cookie throughhttpsec.Carrier.CookiesNamed(session.CookieReader).Extractor— pulls raw credentials from aCarrierinto an unauthenticatedAuthentication. Returns(nil, nil)when its scheme is absent (engine tries the next);(nil, err)when present-but-malformed.Authentication— immutable snapshot of a security context (Principal, Credentials, Authorities, IsAuthenticated, Name). Every state change produces a new value; implementations MUST NOT be mutated. Safe for concurrent reads with no synchronization.Authenticator— two-step:Supports()(cheap type switch, no I/O) thenAuthenticate()returns a new authenticated value or a wrapped sentinel error.Manager— chains authenticators, first-success-wins in registration order; joins errors; returnsErrUnsupportedCredentialwhen none support the credential.Engine— top-level entry point: runs extractors, hands the result to the Manager, returns a context enriched viaWithAuthentication.- Authorization —
VoterreturnsDecision(Grant/Deny/Abstain) over a set ofAttributes;AccessDecisionManagercombines votes with anaffirmative/consensus/unanimousstrategy (named after Spring Security's). The attributes a route lists are the policy: each voter votes only on the attributes it supports — family AND value (HasRole("ADMIN")abstains onRole("USER")) — and the ADM denies when an attribute is supported by no voter. Roles are matched only asROLE_<name>authorities and scopes only as onescope:<name>authority each (security.RolePrefix/security.ScopePrefix). Stock voters live invoter/(HasRole,HasAnyRole,HasScope,HasAnyScope,HasAuthority,HasAnyAuthority,HasPermission, andAuthenticated/Anonymous, which vote only onsecurity.IsAuthenticated/security.IsAnonymous); compose them withAnd(strict) /Or/Not(never grants an unauthenticated principal).
Conventions baked into the core:
- Fail closed by default. No credentials →
Anonymous(); voters deny unless one explicitly grants. The HTTP middleware rejects with 401 unlessWithAnonymousFallbackis set. - Errors are sentinels (
errors.go) implementing the unexportedSecurityErrormarker. Always wrap withfmt.Errorf("...: %w", ErrXxx); callers match witherrors.Is/errors.As, never string matching. The transports answer fromsecurity.ErrorClass(err)(one precedence for joined errors): a credential verdict is 401,ErrUnavailable503, an access denial 403 (401 for an unauthenticated principal), and an error wrapping no sentinel 500 — never a verdict on the credentials. - Context first.
context.Contextis the first argument of every runtime operation (Extract,Authenticate,Hasher.Hash/Verify,TokenVerifier.Verify). It also carries theAuthenticationunder an unexported key —WithAuthentication/FromContext(returnsAnonymous()when absent). - OTel spans live directly in each module — there is intentionally no
EventSinkabstraction and no separateotel/module. The core uses scopegithub.com/hyperscale-stack/security; each instrumented module (httpsec,grpcsec,connectrpcsec,jwtsec,session,samlsp) uses its own. Seedocs/observability.mdfor the span catalog. - Credential checks spend the same work.
basic.Authenticatorruns exactly onehasher.Verifyper attempt — against a dummy hash made by the configured hasher for unknown users, loader failures and accounts without a password — reads the account state after it, and caps concurrent verifications (WithMaxConcurrentVerifications,ErrUnavailablewhen the caller gives up).passwordhashers check a stored hash's parameters (RFC 9106 §3.1 ranges, operator limits) before any hashing. - Sessions are sealed cookies: format v2 (AES-256-GCM under an HKDF key
from ≥ 32 random bytes, associated data = version ‖ cookie name ‖
purpose), default name
__Host-session, absolute lifetimeCreatedAt + TTLnever extended.NewManagerpanics on a configuration browsers would refuse. - Credentials never print: the
basic,bearerandsessionauthentications andsession.Sessionprint and log without their secrets (String,Formatfor every verb,LogValue);session.Sessionalso has a redactingMarshalJSON.
oauth2.NewServer(ServerConfig{...}) aggregates Profile, Storage,
ClientStore, IssuerResolver, Grants, and ClientAuth, and exposes one
http.Handler per RFC endpoint: AuthorizeHandler, TokenHandler,
RevokeHandler, IntrospectHandler, MetadataHandler (endpoint paths
configurable via ServerConfig.RoutePrefix).
Profile(2.0 / 2.0-BCP / 2.1-draft) is enforced at runtime on the grants — PKCE required andplainPKCE refused under BCP/2.1 (PKCE is required of non-confidential clients under every profile); legacypasswordandimplicitflows refused outsideProfile20.- Sub-packages:
grant/(authorization_code,client_credentials,refresh_tokenwith rotation + reuse detection, opt-in legacypassword),clientauth/(client_secret_basic/_post/none),token/(opaque + JWT generators),pkce/. - Fail-closed client policy: a client uses only the grant types
(
GrantTypes, checked at/tokenfor every grant beforeHandle) and requests only the scopes (Scopes) it declares — an empty list allows nothing (oauth2.ClientAllowsGrantType,ErrNoGrantTypesDeclared,ErrNoScopesDeclared);client_credentialsis confidential-only; refresh tokens go to clients declaringrefresh_token. Every test client fixture must declare its grant types and scopes. - Access/refresh tokens and authz codes are stored hashed (
HashToken). - Every authorization is bound to its issuer: every endpoint resolves it
first (the
IssuerResolveris the realm), codes and tokens record it, and theauthorization_code/refresh_tokengrants refuse it elsewhere;/introspectanswers another issuer's token inactive and/revokeleaves it alone. A client implementingIssuerBoundClient(DefaultClient.IssuerValues) is unknown at other issuers — same bytes as an unknownclient_id. - Hardened endpoints: every request is bounded
(
ServerConfig.MaxRequestBytes, 64 KiB;state,nonce,client_idbounds);/authorizesets anti-framing / no-store /Referrer-Policy: same-originheaders, answers a POST with 303, validates redirect URIs (ValidateRedirectURI) and, withAuthorizeConfig.TrustedRedirect, redirects no error of an untrusted client before authentication. Every failed client authentication answers the sameinvalid_client("client authentication failed", detail inOnError); aClientStoreerror not carryinginvalid_clientis an outage (500/503), not an unknown client. - Extension points (
ServerConfig.Extension,oauth2.Extension): the contractoidc/providerbuilds on —ValidateAuthorizeRequestafter the core checks,CompleteAuthorizationturningConsent.ExtensionResultinto anExtensionGrant(narrowing scope + opaqueExtensionData≤ 4 KiB, copied onto every token, unchanged across refreshes),ExtendTokenResponsebetween minting and the single atomic write (typedid_token). Protocol refusals go throughConsent.Refusal(oauth2.ErrLoginRequired, …);grant.Config.RefreshTokenScopegates refresh tokens on a scope (offline_access).oauth2never imports OIDC/JOSE. Storageimplementations:oauth2/storage/memory(dev/tests),oauth2/store/sql,oauth2/store/redis. Every implementation must pass the sharedoauth2/storetestconformance suite;store/sql'sMigrateonly creates tables — schema additions ship asALTERstatements inMIGRATION.md.
oidcprovider.New(oauth2.ServerConfig{...}, oidcprovider.Config{Keys, Claims, Interaction}) builds the oauth2.Server with the provider as its
Extension and owns its ConsentFunc; Provider.Paths(issuer) gives the
mount path of every handler (AuthorizeHandler, UserInfoHandler,
DiscoveryHandler, JWKSHandler, plus the server's own).
- The application implements
Interaction(login/consent UI;prompt=nonegoes toAuthorizeSilently, without aResponseWriter),ClaimsSource(ErrUnknownSubjectstops a grant) andKeySource(per-issuerKeyRing, validated on every use, RS256 required). - The provider enforces the protocol itself: request validation (OIDC Core
§3.1.2),
Request.Checkon every approval (realm,prompt=login,max_age, designated subject, essentialacr),offline_accessconsent (Core §11), ID Tokens at code exchange and refresh (Core §12.2), UserInfo with RFC 6750 errors. - The grant context (authentication event + released claims) travels in the
core's
ExtensionData— no storage of its own. docs/oidc-provider.mdis the exact feature matrix (no certification claimed);internal/interop/oidcopchecks interop with go-oidc.
examples/oauth2/main.go is the canonical wiring example of the library;
examples/oidc-provider the one for the OpenID Provider (login and consent
pages, CSRF, sessions); examples/ also has basic-http, bearer-jwt,
grpc-bearer, connectrpc-bearer, session-web, oidc-client and saml-sp demos.
oidcclient.New(ctx, Config{...}) builds the Relying Party of one
connection (issuer + client registration + redirect URI) after discovery;
Begin/Complete run the Authorization Code flow with PKCE S256, state and
nonce, and StartLogin/HandleCallback wrap them for net/http (binding
cookie included). UserInfo fetches claims whose sub must match.
- Invariants: the issuer comes from configuration only; every fetch goes
through
outbound; token and UserInfo requests follow no redirect, and discovery / JWK Set redirects never leave the origin requested (sameOriginRedirect); a login is bound to the user agent by a binding secret from which the nonce and PKCE verifier are derived (the store holds no secret) and which, with a REQUIRED server-held key (Config.TransactionKeys: the first MACs, all verify), keys the transaction's integrity MAC (integrityFor, verified inCompletebefore any stored field is used; every store persistsIntegrity); a transaction is consumed once, in the pinned order ofTransactionStore.Consume;Callback.URLmust be the connection's redirect URI — scheme, host, port, path and its query parameters (mix-up defense — redirect URIs are distinct per connection;HandleCallbacktakes the host fromr.Hostand the scheme from the redirect URI); nothing is returned before the ID Token is fully validated; every JSON document goes through the strict decoder (strictjson.go); errors and everyfmt/slogrendering (format.go:Format,String,GoString,LogValue) never carry secrets, nor doesencoding/jsonofConfig,AuthRequest,Callback(MarshalJSON) — exceptIdentity/Tokens, JSON-serialisable by design (persistence), which slog's JSON handler writes in full when nested in another value. - Every
TransactionStore(storage/memory,store/sql,store/redis) must passoidc/client/storetest.RunConformance. - Tests use the scriptable fake provider of
fakeop_test.go;internal/interop/oidcrpruns the client against zitadel/oidc and a pinned corpus of real providers' discovery documents (testdata/metadata); its Chrome test runs withOIDC_BROWSER_TESTS=1, its live discovery check withOIDC_LIVE_DISCOVERY=1(Internet, no credentials). internal/interop/oidcstackruns the client against the provider over HTTP — flows, attacks, failures, concurrency — importing no third-party OIDC, OAuth 2.0 or JOSE library (TestDependencyPolicy; the client's own go-jose is part of the code under test); its real-backend store runs (OIDCCLIENT_POSTGRES_DSN,OIDCCLIENT_MYSQL_DSN,OIDCCLIENT_REDIS_ADDR,compose.yaml) are opt-in locally and run in CI throughmake test-backends.TestDocumentedTestsExistfails whendocs/oidc-{conformance,client,provider}.mdcite a test that does not exist, or when a page claims a certification.- Opt-in, never in CI:
make e2e-oidc(e2e/oidc/*.tales, Tales v0.1.0-rc.23 + Chrome, againstexamples/oidc-providerandexamples/oidc-client),make e2e-oidc-live(LIVE_OIDC_*), andmake conformance(the OpenID Foundation suite,OIDF_SUITE_DIR;internal/interop/oidcstack/conformance: the expectations in itstestdatamust be updated with a rationale when a result changes).docs/oidc-conformance.mdrecords their results and the release checklist.
samlsp.New(Config{...}) takes a ConnectionResolver (each Connection has
its own SP entity ID, ACS URL and trusted IdentityProvider), a
TransactionStore, a ReplayCache and the REQUIRED TransactionKeys (1 to
4 server-held keys of at least 32 bytes; every stored transaction carries
their HMAC, verified before any field is used). BeginLogin (HTTP-Redirect
AuthnRequest + __Host-samlsp browser-binding cookie) and CompleteLogin
(HTTP-POST ACS) are the flow; Metadata/ServeMetadata publish SP
metadata; ParseIdentityProviderMetadata imports trusted IdP metadata, and
FetchIdentityProviderMetadata/MetadataSource fetch it from a configured
URL through outbound (pinned entity ID, bounded refresh, never triggered
by response content).
- Validation is layered: streaming lexical gate (
xmlgate.go) → structural profile on the raw etree (precheck*.go) → transaction lookup: integrity MAC (integrity.go), browser binding, expiry → status and ResponseInResponseTo→ atomic consumption (one verification per login) → gosaml2/goxmldsig verification (verify.go) → processing rules on the verified decoded values (postcheck.go) → replay mark. Never read identity data from the raw tree; never write XML-DSig/C14N code. - Strict by default: SHA-1 only via
Connection.InsecureAllowSHA1; ambiguous shapes and encrypted assertions refused with typed errors. Every refusal matchesErrRejected(wrapssecurity.ErrInvalidCredentials); store/resolver failures do not. Storageimplementations:saml/sp/storage/memory(dev/tests),saml/sp/store/sql,saml/sp/store/redis; all must passsaml/sp/storetest.saml/sp/testdata/corpuspins 390 entries over 360 signed adversarial responses, stored uncompressed (review them as text diffs); after a deliberate change regenerate it withgo test -run '^TestCorpusGenerate$' -count=1 . -args -corpus.update(insaml/sp): regeneration is byte-identical only with the toolchain recorded in the manifest (generatedWith); with another Go version every file changes (new RSA keys), so regenerate in a commit of its own.TestCorpusCoversEveryRulerequires every refusal rule of the sources in some entry (or in its documented allowlist).internal/interop/samlruns the SP against crewjam's IdP, replays the pinned Keycloak responses (testdata/keycloak-*), races instances on shared stores and tests metadata retrieval. Opt-in, withinternal/interop/saml/compose.yaml:SAMLSP_KEYCLOAK_URL+SAMLSP_KEYCLOAK_ADMIN_PASSWORD(TestKeycloakLive;-args -keycloak.capturerewrites the 26.7.4 captures, and only-args -keycloak.fixtureagainst Keycloak 26.0.7 rewrites the 26.0.7 fixture),SAMLSP_REDIS_ADDR,SAMLSP_POSTGRES_DSN,SAMLSP_MYSQL_DSN— CI runs the shared-store backends throughmake test-backends.e2e/saml/*.tales(make e2e,e2e/saml/run.sh) drivesexamples/saml-spin Chrome; its sites are served byTestE2EHelperProcess(skipped unlessSAMLSP_E2E_SERVEis set). CI never runs it: re-runmake e2eafter any change to the example or tosamlsp's cookie, login or ACS code. The integration guide isdocs/saml-sp.md.
- Mocks:
make generateruns mockery v3 (pinned intools/mockery, configured by.mockery.yaml). Every interface of the library packages gets an in-packagemock_<interface>_test.go(Mock<Interface>, testify template,EXPECT()API). These files exist only in test binaries, are never committed (.gitignore:mock_*) and are regenerated bymake generate, whichmake test/lint/vuln/tidyand CI run first. Use them for interface doubles in new tests; migrate a hand-written fake when you touch its test (no big-bang rewrite). A module whose packages get mocks keeps amockery_test.gothat importstestify/mock, so itsgo.mod/go.sumare the same with or without generated mocks (Dependabot andgo mod tidynever generate them). Every module builds withGOWORK=offwithout them; only its tests need them, so the tests of a published module only compile aftermake generatein a repository checkout (a module zip has neither the mocks nor.mockery.yaml). Never hand-write aMock<Interface>identifier or amock_*file (git-ignored). - Tools: one module per tool under
tools/(golangci-lint, govulncheck, goveralls, mockery), outsidego.work, so their dependencies never reach a librarygo.modand each tool builds with exactly its own tested dependency set.make toolsinstalls them intobuild/bin(checksum-verified by each tool'sgo.sum). Bump one withcd tools/<name> && GOWORK=off go get -tool <package>@<version>. - Lint:
golangci-lintv2.14.0,default: none+ ~30 explicitly-enabled linters includinggosec,wrapcheck,errorlint,wsl_v5,forcetypeassert.gocyclomax complexity 18. Tests are excluded from lint (run.tests: false). All wrapped errors must keep%w. - Go 1.27.2 in every
go.modand ingo.work(CI installs the versiongo.worknames). Indentation: tabs in.goandMakefile, 4 spaces elsewhere, 2 spaces in YAML (see.editorconfig). All source files carry the MIT copyright header.