Skip to content

feat(socrate): export ServiceToken for calling other services as the app - #78

Merged
ovander merged 3 commits into
mainfrom
feat/socrate-service-token
Oct 3, 2026
Merged

ovander merged 3 commits into
mainfrom
feat/socrate-service-token

Conversation

@ovander

@ovander ovander commented Oct 3, 2026

Copy link
Copy Markdown
Owner

What and why

Adds (*socrate.Client).ServiceToken(ctx) (token string, expiresAt time.Time, err error). It returns the application's client_credentials access token and its expiry: the same cached token the service-account calls already use. The token is exchanged again within 30 s of expiry, and concurrent callers share one exchange. The unexported getServiceToken now wraps it, so there is one exchange path.

Requested by Lakebridge. Its consumer client (EL-LAKE-SPEC-004, NewSocrateTokenSource(c *socrate.Client)) calls the gateway with the consumer app's Socrate service token. Until now the token could only be had by re-implementing the OAuth exchange outside backendkit.

A 200 without an access_token is now an error instead of an empty token that is cached and sent (fail closed).

How it was tested

New socrate/service_token_test.go:

  • Returns the token and an expiry about one hour ahead.
  • The second call is served from the cache.
  • 50 concurrent callers trigger one exchange.
  • A token valid under 30 s is exchanged again.
  • Error cases: a rejected exchange (the secret never appears in the error), an empty access_token, no ClientSecret, a cancelled context.

The existing TestGetServiceToken_ExchangesCredentials still passes through the wrapper.

  • 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, built with Go 1.27.1) reports no issue
  • govulncheck ./... reports no vulnerability (run against a database built from golang/vulndb; vuln.go.dev is not reachable from my environment)
  • A line is added under ## [Unreleased] in CHANGELOG.md

README (socrate section) and docs/CLIENT-INTEGRATION.md (M2M method table, port table and service-account section) document the method.

Compatibility

  • Exported-API change: yes, new method (*socrate.Client).ServiceToken. No existing symbol changes.
  • Behaviour change for existing callers: a token response without access_token is an error. Before, an empty token was cached and every service-account call failed at Socrate, so no working caller is affected.
  • Breaking change: none (minor release; the owner tags it, intended as v1.19.0).

🤖 Generated with Claude Code

https://claude.ai/code/session_014dfgURbaXrGaV6cdxw8JBt


Generated by Claude Code

claude added 3 commits October 3, 2026 18:28
ServiceToken(ctx) returns the cached client_credentials access token and its expiry; the
unexported getServiceToken now wraps it. A token response without access_token is an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dfgURbaXrGaV6cdxw8JBt
…n is absent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dfgURbaXrGaV6cdxw8JBt
…es_in from the request

Review of #78. The returned expiry is what a caller relies on, so it must
never be later than the real one:
- the token's own exp claim comes first, as the instant every verifier checks;
- otherwise expires_in is counted from just before the request is sent, not
  from after the response (which was late by the round trip);
- a response with neither is trusted for one minute instead of a guessed 55
  minutes, longer than a Socrate token lives.

Tests: exp preferred over expires_in; expires_in counted from the request
(slow server); the no-expiry fallback stays within a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKRxaeYxyDhmt42cehLsGA

ovander commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Review: the change is sound (one exchange path, fail closed on a missing access_token, the secret kept out of errors, and the cache shared under the lock). One fix concerned the expiry, which is the value Lakebridge relies on. It must never be later than the real one. I pushed d6bb2e8:

  • The token's exp claim comes first. It is the instant every verifier checks.
  • Otherwise expires_in counts from just before the request is sent, not from after the response. Counting from the response was late by the round trip.
  • A response with neither field is trusted for 1 minute, not a guessed 55 minutes, which is longer than a Socrate token lives.

New tests: exp beats expires_in; expires_in counts from the request (checked with a 300 ms slow server); the fallback stays within a minute. The fallback test now asserts the 1-minute limit instead of 55 minutes. CHANGELOG, the guide and the doc comment are updated to match. Local checks: build, vet, go test -race ./..., golangci-lint v2.14.0 (0 issues), and go mod tidy (no change).


Generated by Claude Code

@ovander
ovander merged commit d8c636c into main Oct 3, 2026
6 checks passed
@ovander ovander mentioned this pull request Oct 3, 2026
7 tasks done
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