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
3 changes: 0 additions & 3 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,3 @@ contact_links:
- name: Report a security vulnerability
url: https://github.com/ovander/backendkit/security/advisories/new
about: Report privately; please do not open a public issue.
- name: Problem with the Socrate server itself
url: https://github.com/ovander/go-oauth2/issues
about: Issues in the identity provider belong in ovander/go-oauth2.
21 changes: 13 additions & 8 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@

## How it was tested

<!-- New or changed tests, and anything checked by hand. -->
<!-- New or changed tests, and anything checked by hand. The same checks as CI: -->

- [ ] `go build ./...`, `go vet ./...`, `go test -race -count=1 ./...` pass
- [ ] `golangci-lint run ./...` reports no issue
- [ ] `go mod tidy` leaves `go.sum` unchanged
- [ ] `go mod tidy && git diff --exit-code go.sum` leaves `go.sum` unchanged
- [ ] `go build ./...` passes
- [ ] `go vet ./...` passes
- [ ] `go test -race -count=1 -timeout=120s ./...` passes
- [ ] `golangci-lint run ./...` (v2.14.0) reports no issue
- [ ] `govulncheck ./...` reports no vulnerability
- [ ] A line is added under `## [Unreleased]` in `CHANGELOG.md`

## Compatibility

<!-- Delete what does not apply. -->
- Exported API: <!-- new symbols / changed signatures / none -->
- Behaviour change for existing callers: <!-- e.g. a new default, a stricter check -->
- Breaking change: <!-- none, or why it needs a new major version -->
<!-- Delete what does not apply. Within v1: no removed or renamed exported symbol, no changed
signature, no stricter default that breaks a working caller without an opt-in. -->

- Exported-API change: <!-- yes (list the new or changed symbols) / no -->
- Behaviour change for existing callers: <!-- e.g. a new opt-in option, a stricter check -->
- Breaking change: <!-- none, or why it needs a new major version (v2) -->
77 changes: 41 additions & 36 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Changelog

All notable changes to backendkit are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All notable changes to backendkit are documented here. Format:
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
[Semantic Versioning](https://semver.org/).

## [Unreleased]

Policy enforcement for applications (Socrate plan A4, part 2), and a patched
build toolchain. Both purely additive for consumers.
Policy enforcement for applications against Socrate's central policy decision
point, and a patched build toolchain. Both purely additive for consumers.

### Added

Expand Down Expand Up @@ -39,6 +39,9 @@ build toolchain. Both purely additive for consumers.

### Changed

- **Documentation uplift:** the README, `docs/CLIENT-INTEGRATION.md` (new BFF and `pep`
sections; a BFF is now the recommended path for browser apps), `SECURITY.md`, `CONTRIBUTING.md`
and the GitHub templates follow the Socrate suite's documentation standard.
- **Build toolchain: Go 1.26.8 → Go 1.27.1** (`toolchain` directive and CI).
The `go 1.25.0` minimum is unchanged: consumers are unaffected, and the
GODEBUG defaults this module's own tests run with stay those of Go 1.25.
Expand All @@ -53,14 +56,15 @@ build toolchain. Both purely additive for consumers.
### Fixed

- README and `docs/CLIENT-INTEGRATION.md` linked Socrate to a repository that does not exist;
they now point to `ovander/go-oauth2`. The README's contributing rule on cross-package imports
now matches the code (`bff`/`pep` → `socrate`, `aigateway` → `ailang`).
they now name `ovander/go-oauth2` in plain text, as it is not public yet. The README's
contributing rule on cross-package imports now matches the code (`bff`/`pep` → `socrate`,
`aigateway` → `ailang`).

### Removed

- The internal review documents (`CTO-ARCHITECTURE-REVIEW.md`, `FRAMEWORK-EVOLUTION.md`,
`SECURITY-ARCHITECTURE.md`, `SECURITY-AUDIT.md`) left the public tree. The fixes they led to
remain listed below with their finding IDs.
- The internal review documents (the architecture review, the framework-evolution notes, the
security architecture review and the security audit) left the public tree. The fixes they led
to remain listed below with their finding IDs.

## [1.13.0] - 2026-09-04

Expand All @@ -81,9 +85,8 @@ service lands on one dashboard. Purely additive.

## [1.12.0] - 2026-09-03

Shared-gateway hardening from the Socrate suite pass-3 audit
(`CR-socrate-suite-security-pass3.md`, `go-oauth2` repo). Additive except
for one behaviour change called out below.
Shared-gateway hardening from the third-pass security review of the Socrate
suite. Additive except for one behaviour change called out below.

> **Behaviour change (P3-12):** `Gateway.ProxyWithSession` no longer deletes
> the session on *every* refresh error. Only a refresh the authorization
Expand Down Expand Up @@ -179,23 +182,23 @@ for one behaviour change called out below.
called `ConstantTimeCompare` directly, so a session that somehow lost its
CSRF value (`csrf == ""`) matched an empty request token, silently
disabling CSRF protection for that session. `MatchCSRF` now always returns
`false` when the stored value is empty. Addresses **P2-6**
(`CR-socrate-suite-security-pass2.md`, upstream `go-oauth2` repo).
`false` when the stored value is empty. Addresses **P2-6** (second-pass
security review of the Socrate suite).

- **bff: `Gateway`'s zero value is now fail-closed.** `Gateway.AuthEnabled`
defaulted to `false`, so a bare `&Gateway{...}` struct literal — no field
set — was a fully-open pass-through, contradicting this package's
documented "fail-closed by default" behaviour. The field is renamed and
inverted to **`DisableAuth`**, so the zero value now means "auth enforced."
Addresses **P2-7** (`CR-socrate-suite-security-pass2.md`).
Addresses **P2-7** (second-pass security review).

- **bff: coalesce concurrent token refreshes per session.** Concurrent
`EnsureFresh` calls near token expiry could each independently spend the
same single-use rotating refresh token; only the first succeeded and the
rest tore down the session. `EnsureFresh` now coalesces concurrent calls
per session ID via `singleflight.Group` (mirroring the `jwtauth` H-1 JWKS
fix), with every waiter re-checking token validity before spending a
refresh. Addresses **P2-8** (`CR-socrate-suite-security-pass2.md`).
refresh. Addresses **P2-8** (second-pass security review).

### Migration

Expand Down Expand Up @@ -244,13 +247,13 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
inside the window returns key-not-found without a network call, and (3) backed
by a short negative cache (default 30s; `WithNegativeCacheTTL`) for recently-seen
unknown kids. A legitimately rotated key still resolves: the first miss after the
cooldown triggers exactly one refetch. Addresses **H-1** (`SECURITY-AUDIT.md`).
cooldown triggers exactly one refetch. Addresses **H-1** (internal security audit).

- **jwtauth: require `exp` and add clock-skew leeway.** The parser now sets
`jwt.WithExpirationRequired()`, so a token minted without an `exp` claim (which
would otherwise never expire) is rejected, plus `jwt.WithLeeway` (default 60s;
`WithLeeway`) for time-based claim validation. Addresses **M-2**
(`SECURITY-AUDIT.md`).
(internal security audit).

- **socrate: complete path-segment escaping (corrects the F-7 ledger).** v1.9.0
escaped only `client.go`; the remaining admin/monitoring/alerts/reports methods
Expand All @@ -259,7 +262,7 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
`alerts.go` and `reports.go` (user/app/superadmin/blocked-ip/ip-reputation/log/
alert-rule/report IDs). Internal server-resolved values (e.g. the app ID) are
intentionally left unescaped, as in `client.go`. Addresses **M-1** and corrects
the previously overstated **F-7** "Fixed" claim (`SECURITY-AUDIT.md`).
the previously overstated **F-7** "Fixed" claim (internal security audit).

## [1.9.0] - 2026-06-20

Expand All @@ -269,16 +272,16 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
warning at construction when the issuer is empty, so a service running without
`iss` enforcement is visible at startup instead of silently fail-open. No change
to token validation; making issuer mandatory remains a v2.0 default-flip.
Addresses **F-5** (`SECURITY-AUDIT.md`).
Addresses **F-5** (internal security audit).
([#30](https://github.com/ovander/backendkit/issues/30))

- **apierror: redact internal message/details on 5xx responses.** `WriteJSON` now
replaces the dev-facing `Message` with a generic status text and drops `Details`
for any 5xx response, so internal detail (e.g. `apierror.Internal(err.Error())`)
can no longer leak to clients. **4xx responses are unchanged.** The full error
is still available server-side via `Error()` for logging; the struct doc was
corrected. Addresses **F-17 / INV-9** (`SECURITY-AUDIT.md`,
`SECURITY-ARCHITECTURE.md`).
corrected. Addresses **F-17 / INV-9** (internal security audit
and architecture review).
**Behaviour change:** 5xx response bodies no longer echo the supplied message.
([#28](https://github.com/ovander/backendkit/issues/28))

Expand All @@ -287,23 +290,23 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
timing/row-count/caller). GORM hands the logger SQL with bound parameter values
already interpolated — which can contain PII or secrets — so production loggers
should enable it. Opt-in; default behaviour unchanged. Addresses **F-9 / INV-10**
(`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
(internal security audit and architecture review).
([#26](https://github.com/ovander/backendkit/issues/26))

- **jwtauth / socrate / aigateway: bound upstream response reads.** All reads of
upstream HTTP bodies are now capped with `io.LimitReader` — JWKS at 1 MiB,
Socrate and AI-provider responses at 10 MiB — so a compromised/MITM or oversized
upstream cannot exhaust memory. `socrate.readBody` returns an explicit error when
the cap is exceeded. Normal-size responses are unaffected. Addresses
**F-8 / INV-12** (`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
**F-8 / INV-12** (internal security audit and architecture review).
([#24](https://github.com/ovander/backendkit/issues/24))

- **socrate: path-escape `userID` in request URLs.** `socrate.Client` now wraps the
caller-supplied `userID` in `url.PathEscape` at every endpoint that interpolates
it (`GetUser`, `UpdateUserRole`, `DeleteUser`, `ResendVerification`,
`ForcePasswordReset`, `GetUserAsService`), so an ID containing `/`, `?`, `#`, or
`..` can no longer rewrite the target route. Addresses **F-7 / INV-11**
(`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
(internal security audit and architecture review).
([#22](https://github.com/ovander/backendkit/issues/22))

## [1.8.0] - 2026-06-20
Expand All @@ -315,7 +318,7 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
exponent (odd, > 1, within `int` range) instead of silently truncating it, so a
JWKS serving an undersized or malformed key is no longer trusted. Backward
compatible for real deployments (Socrate/RS256 use ≥2048-bit keys). Addresses
**F-10 / INV-13** (`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
**F-10 / INV-13** (internal security audit and architecture review).
([#18](https://github.com/ovander/backendkit/issues/18))

- **deps: bump `golang-jwt/jwt/v5` `v5.2.1` → `v5.2.2`.** Clears GO-2025-3553
Expand Down Expand Up @@ -343,7 +346,7 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
`token_version` (logout / password-change / admin revocation) or a `jti` denylist
— checks that local signature validation alone cannot. Opt-in: with none
configured, a token stays valid until `exp` as before. Addresses **F-2 / INV-3**
(`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
(internal security audit and architecture review).
([#16](https://github.com/ovander/backendkit/issues/16))

- **httpware: `RequireTenant` middleware.** A plain
Expand All @@ -352,7 +355,7 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
tenant-scoped handlers can never run against the nil tenant. Opt-in; mount it
after the auth middleware on tenant-scoped route groups. Rejections are logged
through the request-scoped logger. Addresses **F-3 / INV-6**
(`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
(internal security audit and architecture review).
([#14](https://github.com/ovander/backendkit/issues/14))

- **jwtauth: opt-in JWT audience (`aud`) validation.** New `jwtauth.Option`
Expand All @@ -361,7 +364,7 @@ gw := &bff.Gateway{Store: store, Cookie: cookie, Refresher: r}
expected audience (typically the service's OAuth `client_id`), closing the
cross-app token-replay exposure where one Socrate-issued token was valid at
every service sharing the same issuer and JWKS.
Resolves **F-1 / INV-2** (`SECURITY-AUDIT.md`, `SECURITY-ARCHITECTURE.md`).
Resolves **F-1 / INV-2** (internal security audit and architecture review).
([#6](https://github.com/ovander/backendkit/issues/6))

### Migration
Expand All @@ -388,9 +391,11 @@ are rejected. Confirm your Socrate server populates `aud` before enabling it in
production. Making audience validation required-by-default is deferred to a future
major (v2.0) and tracked separately.

### Notes

- `govulncheck` is part of the required quality gates but could not be executed in
the CI sandbox for this change because `https://vuln.go.dev` is blocked by the
environment's network policy. All other gates (`go fmt`, `go vet`,
`golangci-lint`, `go test`, `go test -race`) pass.
[Unreleased]: https://github.com/ovander/backendkit/compare/v1.13.0...HEAD
[1.13.0]: https://github.com/ovander/backendkit/compare/v1.12.0...v1.13.0
[1.12.0]: https://github.com/ovander/backendkit/compare/v1.11.1...v1.12.0
[1.11.1]: https://github.com/ovander/backendkit/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/ovander/backendkit/compare/v1.10.0...v1.11.0
[1.10.0]: https://github.com/ovander/backendkit/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/ovander/backendkit/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/ovander/backendkit/compare/v1.7.0...v1.8.0
23 changes: 14 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Contributing to backendkit

Thank you for your interest. backendkit is the shared Go library of the Socrate suite: it lets a
Go service authenticate users against the Socrate OAuth 2.1 / OIDC provider
([`ovander/go-oauth2`](https://github.com/ovander/go-oauth2)), run a Backend-for-Frontend, and
enforce Socrate's policies. Contributions are accepted under the project's licence,
Thank you for your interest. backendkit is part of the Socrate suite: it is the shared Go library
that lets a service validate tokens from Socrate, the suite's OAuth 2.1 / OpenID Connect server
(`ovander/go-oauth2`, not public yet), run a Backend-for-Frontend, and enforce Socrate's central
policy decisions. Contributions are accepted under the project's licence,
[Apache-2.0](LICENSE).

## Development setup
Expand All @@ -26,6 +26,8 @@ go test ./...
discussing it first.
- Every exported symbol has a doc comment that begins with its name. Runnable examples go in
`example_test.go`; they appear on pkg.go.dev.
- A new package gets a package doc comment, a row in the README package tables and a section in
the README package reference.
- Security-relevant behaviour fails closed by default (for example, `bff.Gateway` answers 401
without a valid session instead of passing the request through). An opt-out must be an
explicit, documented option.
Expand All @@ -46,16 +48,19 @@ govulncheck ./...
```

- Tests sit next to the code (`*_test.go`), table-driven.
- A bug fix comes with a test that fails without it.
- Do not weaken a check to get green: no skipped or deleted tests, and no `//nolint` or `t.Skip`
without a one-line reason.

## Pull requests

1. Branch from `main` (`feat/…`, `fix/…`, `chore/…`, `docs/…`).
1. Branch from `main` (`feat/…`, `fix/…`, `chore/…`, `ci/…`, `docs/…`). Keep one change per
pull request.
2. Commit with [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`,
`chore:`, `docs:`, `ci:`, `test:`).
3. Add a line under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md).
4. Open the PR with the template filled in, including any change to the exported API.
5. CI must be green. The maintainer reviews and merges.
3. A bug fix comes with a test that fails without it.
4. Add a line under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md).
5. Open the PR with the template filled in, including any change to the exported API.
6. CI must be green. The maintainer reviews and merges.

## Releases

Expand Down
Loading
Loading