Notable changes to this service, newest first, per release. This file is written for whoever runs the service or integrates against it.
A security release. The service now builds with Go 1.27.2, and its libraries move to their Go 1.27.2 releases. Nothing about the API, the configuration or the database changes.
- Go 1.27.2 (was 1.27.0), both in
go.modand as the image's default build toolchain (ARG GO_VERSIONin theDockerfile). Go 1.27.2 andgolang.org/x/netv0.60.0 fix vulnerabilities that this service's code reaches under 1.27.0:govulncheckfound nine it calls, GO-2026-6603, -6605, -6607, -6608, -6610, -6611, -6612, -6613 and -6617, innet/http,crypto/tls,net/textprotoandgolang.org/x/net. Under the new set it finds none. Rebuild the image to pick the fixes up; an image built before this change still carries them. - Libraries:
go-authbyte→ v0.29.1 (from v0.23.1),go-platform-kit→ v1.11.4,go-sec-events→ v1.2.2,go-gdpr-audit→ v1.1.6, azugo → v0.40.0,go-redis→ v9.23.0,fasthttp→ v1.75.0, plus 39 indirect modules with them. The go-authbyte releases in between add features this service does not use, with one visible change: when the authorization server refuses a token request, the error no longer repeats the server's answer, so a log line built from that error stops carrying it (the message is nowthe token call responded <status>). None of azugo v0.40.0's behaviour changes reaches this service: it sets no cookies, its redirects (on to the identity provider, back to the application, and on logout) all answer GET requests, so the new303after a non-GET request does not apply to them, it makes no outbound calls through azugo's HTTP client and it implements no azugo cache.
- CI's linter moved to golangci-lint v2.14.0. The earlier release cannot read Go 1.27.2's compiled standard library and stops before linting anything.
- The gate is green on Go 1.27.2:
go mod verify,go mod tidy -diff, build, vet,gofmt, golangci-lint v2.14.0,go test -racewith 0 races;govulncheckreports no vulnerabilities found.
A Web eID login (the card in a reader) now permits cscEidPlugin besides webEid; an eID Scan login (the card
read by a phone) permits cscEidScan besides eidScan. Neither login reaches the other card route. The
session's permitted_flows:
{ "login_method": "webEid", "permitted_flows": ["webEid", "cscEidPlugin"] }
{ "login_method": "eidScan", "permitted_flows": ["eidScan", "cscEidScan"] }The CSC remote-signing flow authenticates with the eID card only (read by a phone, or in a card reader), so
it is no longer among the flows an eParaksts Mobile login may drive. The single csc flow name is also
retired in favour of two, cscEidScan and cscEidPlugin, named for how the card is read. The session's
permitted_flows for an eParaksts Mobile login:
{ "login_method": "eparakstsMobile", "permitted_flows": ["eparakstsMobile", "eparakstsMobileEseal"] }A Web eID or eID Scan login permits the CSC flow that reads its card the same way (above).
With a membership register wired, the register's resolve answer now has to name the subject it is
about (subjectKey), and it has to be the subject asked about. An answer about anyone else, or one
that names nobody, refuses the token instead of minting its memberships:
POST /token (a user token, register wired)
→ 502 { "code": "err:upstream:unavailable" } when the register's answer is not about this person
Every resolve also writes one info line, membership resolved, with the key asked, the key
answered for, and each membership's tenant and scopes. Upgrade the register first: a register
that does not yet name its subject makes every login that needs a membership fail this way.
Signing out through GET /logout used to send every upstream login on through the provider's
end-session endpoint, ending the session the provider keeps in the browser. That is right for a
provider whose short-lived SSO session on a shared device would otherwise sign the next person in
as the previous one — the eParaksts profile keeps doing it — and wrong for a directory provider,
whose session is the person's whole estate: signing out of this application signed them out of
their mail and documents too, after a page asking which account to sign out of. The generic
connector now signs out locally by default: this service's session ends and the browser
returns to redirect_uri; the provider's session is left alone. A deployment that wants the
front-channel hop for a generic provider lists the methods:
OIDC_UPSTREAM_METHODS_FEDERATED=upstream
The logout audit event's federated attribute says which of the two happened. Nothing changes for
the eParaksts profile.
When the upstream provider publishes a key set — its discovery document names a jwks_uri, as
every mainstream provider's does, or OIDC_UPSTREAM_JWKS_URL and OIDC_UPSTREAM_ISSUER are set
for a provider configured by explicit endpoints — every login must now carry an id_token that
verifies: signed with one of the published keys (RS256 · PS256 · ES256 only), iss equal to the
issuer, aud containing this client, exp and iat within TOKEN_CLOCK_SKEW_LEEWAY, the nonce
this service sent with the authorize request, and a subject equal to userinfo's. A missing or
failing id_token refuses the login — 401, the reason in the login-failure audit event, no claim
value in it. The id_token's claims win over userinfo's where both carry one, and its acrs join
the amr list for the LOA_POLICY vocabulary. Without a key set nothing changes: the eParaksts
profile and any provider with fixed paths and no jwks_uri stay userinfo-only.
Two claim maps for an organisation whose people sign in through its own directory:
OIDC_UPSTREAM_CLAIM_DIRECTORY_ID (default sub; oid for Microsoft Entra ID) names the durable
identifier the person's credential is stored under, and OIDC_UPSTREAM_CLAIM_ACCOUNT_STATUS
(acct for Entra) with OIDC_UPSTREAM_ACCOUNT_STATUS_GUEST (default 1) tells a member of the
directory from a guest in it.
OIDC_UPSTREAM_AUTHORITY_URL=https://login.microsoftonline.com/<tenant-id>/v2.0
OIDC_UPSTREAM_SCOPES=openid profile email
OIDC_UPSTREAM_CLAIM_SERIAL= # empty: a directory carries no identity code
OIDC_UPSTREAM_CLAIM_DIRECTORY_ID=oid
OIDC_UPSTREAM_CLAIM_ACCOUNT_STATUS=acct
OIDC_UPSTREAM_METHOD_DEFAULT=upstream
OIDC_UPSTREAM_METHODS_ALLOWED=upstream
OIDC_UPSTREAM_LOA_DEFAULT=low
A login without an identity code is no longer refused. The identity store creates the person
without one, resolved by their credential on every later login; such a person stays separate from
the same human's card login until linked by a deliberate act (a later release). Requires the
platform database at identity/V3 — against an older one the login is refused as before.
At token issue, a person who came through the upstream provider and holds no membership is offered
to the membership register's admission door (POST /api/v1/directory-admissions, on the
membership:claim scope this service already holds toward the register — no registry change):
the tenant that attached that issuer as its directory admits them as an active member with no
grants, and the token is minted for that tenant with an empty scope set — a member who holds
nothing until an administrator assigns a role. A guest in the provider's directory is not offered
and is refused as before (403 err:membership:notMember); so is everyone whose issuer no tenant
attached. A person already holding a membership is never offered, so a refresh costs no extra
call. Requires a register release that exposes the door: an older register answers 404, and
the issue fails closed (502 err:upstream:unavailable) for exactly the people who would have been
admitted — deploy the register first.
Setting both OIDC_UPSTREAM_* and EPARAKSTS_* used to select the generic connector silently.
The service now refuses to start: two upstream identity providers are configured (OIDC_UPSTREAM_
and EPARAKSTS_): this service runs exactly one — unset one family. A deployment that carried
eParaksts placeholders beside a generic connector removes them.
Changed — the membership register is asked by the person's platform subject, never their identity code
At token issue this service asks the membership register which organisations a person belongs to. It
asked by the person's national identity code, typed pno:<code>; it now asks by their platform
subject — the stable id the identity store keys the person on, which is the token's own sub —
typed sub:<person id>. A service account is still asked for by its client id (svc:<client id>).
No token changes shape: the key is a function of sub, and every consumer derives it the same way, so
nothing new is minted and the identity code (serial_number) stays on the token for the signing side.
register lookup, before: claim + resolve by "pno:PNOLV-XXXXXXXXXXX"
register lookup, after: claim + resolve by "sub:01J8X2K4M9N7P3Q5R6S8T0V1W2" (= "sub:" + the token's sub)
Removed with it: the refusal "the login carries no identity code" (a 502 at token issue). A session always has a subject, so a login method that supplies no identity code can be issued a token and resolved against the register like any other.
Deploy order. The register's database migration that retires the pno: kind comes first, then the
register service and this service together. Against an older register this service's sub: keys are
refused by the register's constraint; an older authorization server's pno: keys are refused by the
new register at every write door and resolve to nobody. Neither combination runs; migrate, then redeploy.
An administrator registering or inviting somebody — or recording a person who will never log in — needs that person's platform subject to register them under, and until now a subject existed only once the person had logged in. The new door creates the person row in the identity store with the canonical identity code and whatever name is known, and no credential: a record, not an account. Nothing can log in as them until a login attaches a credential, and that login lands on this row because the same canonical code is matched there.
POST /identity/persons Authorization: DPoP <token carrying identity:admin>
{"identityCode": "PNOLV-XXXXXX-XXXXX", "name": "…", "givenName": "…", "familyName": "…"}
201 {"personSub": "01J8X2K4M9N7P3Q5R6S8T0V1W2", "created": true} the person is new
200 {"personSub": "01J8X2K4M9N7P3Q5R6S8T0V1W2", "created": false} already known (registered, or logged in before);
names are filled only where the row has none
422 err:identity:invalid the code names no country, or an identity type this platform does not know — never echoed
403 err:identity:forbidden the token does not carry identity:admin
The scope is minted like every other: a role in the membership register grants it to an administrator; for bring-up a registry client may hold it as a service-token grant. Each registration is recorded as a GDPR-audit identity write (the administrator as actor, the person as subject), routine and fail-open like the login path's own record. Nothing to configure.
The setting is gone. It supplied a country for an identity-code claim that carried none, so that a
bare national code could be stored in the canonical PNO<CC>-<code> form. Two things were wrong with
it: the variable was never bound to the configuration in the first place, so setting it had no effect
and produced no warning; and the design was unsound even had it worked, because one value applies to
every person who logs in through the provider. A deployment whose users hold codes from more than one
national register would have filed some of them under another register's country — a perfectly valid
key belonging to the wrong person, with nothing to notice it by.
If you set it: nothing changes, because nothing was reading it. No deployment behaviour differs.
What the service does now: the claim named by OIDC_UPSTREAM_CLAIM_SERIAL must carry an identity
code that states its own country (PNOLV-XXXXXXXXXXX). A bare code refuses the login, as it already
did. The remedy is a claim mapping at your identity provider, where the identity type can be
stated alongside the country rather than assumed. Card login is unaffected: it takes the country from
the card certificate's own subject attribute, which is a fact about the person holding the card.
A registered service client can now act for an organisation rather than only as itself. At
POST /token with grant_type=client_credentials, a client that adds tenant=<id> is treated as a
service account: its membership in that organisation is checked in the membership register (the
register key is the client id itself, e.g. svc:acme-dms), the organisation is minted into the token
as tenant, and the scopes are what the client's registry grant allows and its membership grants.
Not a member of that organisation → 403 err:membership:notMember; a member with no role toward the
requested audience → 403; no register configured → 403 err:membership:notMember; register
unreachable → 502 err:upstream:unavailable. A client that names no tenant is unchanged: the registry
alone decides, no register is consulted, no tenant is minted. The issue event records the tenant.
POST /token
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs…
grant_type=client_credentials&client_id=svc:acme-dms&client_secret=…&audience=svc:signbyte-integration-api&scope=signing-requests:write&tenant=01TENANTACMEHTTP/1.1 200 OK
{ "access_token": "eyJ…", "token_type": "DPoP", "expires_in": 300 }The token's claims: sub and client_id svc:acme-dms, tenant 01TENANTACME, scope
signing-requests:write, cnf.jkt.
RFC 8693 token exchange admits a service subject when its token carries a tenant — the token of a
service account, which nothing but a verified membership mints — and carries that tenant into the delegated
token together with the actor chain. A service token without a tenant (a platform service acting as itself)
is refused as before, 403, and the refusal is now recorded as an authorization-denied security event
naming the requesting client and the subject it asked for. People are exchanged exactly as before.
Until now one knob, ROLEBYTE_URL, decided that every person needs a membership. Each browser client's
registry row can now say it for its own people: membership_required: false keeps a client's people on the
baseline scope set with no tenant, and the register is never asked about them; true, or unset with a
register wired, keeps today's behaviour (no membership → 403 err:membership:notMember). One deployment can
therefore serve a public portal and a members-only product from one issuer. A row that requires a membership
in a deployment with no register refuses to start. Existing deployments change nothing: the default with a
register wired is "required", without one "baseline".
public_clients:
- client_id: portal-spa
enabled: true
membership_required: false
allowed_redirect_uris: [https://portal.example/api/portal/v1/login/callback]A person who belongs to several organisations was refused with 409 err:membership:ambiguous. Their token
is now issued with no tenant and no scopes — never the union — and the token response lists the
organisations to choose from in a new tenants member. The client shows the choice and repeats the login
with GET /authorize?tenant=<id>; the choice rides the login into the token issue, is checked against the
person's memberships (an organisation they do not belong to → 403 err:membership:notMember) and the chosen
organisation's scopes and tenant are minted. The session remembers the choice, so a refresh re-resolves the
same organisation. The 409 is no longer produced.
HTTP/1.1 200 OK
{ "access_token": "eyJ…", "token_type": "DPoP", "expires_in": 900, "refresh_token": "…",
"tenants": ["01TENANTA", "01TENANTB"] }Sessions established before this release carry no client id; at their next refresh they are treated as a client that requires a membership. In a deployment whose people are on the baseline, such a session is refused once and the person signs in again.
Publishing a release re-pointed the version tag by rewrapping the image manifest into a new manifest list, which gave the tag a different digest from the one the signature covers — so verifying the signature on a version tag failed. The retag is now a plain pull, tag and push, which keeps the digest and therefore keeps the signature valid. Separately, a release published right after a merge could race the branch build; the job now waits for the image to be published before tagging.
A version tag published before this needs one release re-publish to be re-pointed at the signed
digest. Verifying the rolling :develop or :latest tag was never affected.
A scraper that asked for the OpenMetrics format by sending Accept: application/openmetrics-text
used to be answered in it, with the # EOF terminator that format requires. This service now
answers in the Prometheus text format whatever the scraper asks for, and writes no # EOF:
GET /metrics
Accept: application/openmetrics-text
200 OK
Content-Type: text/plain; version=0.0.4; charset=utf-8The metric names, labels and values are unchanged, so Prometheus — and anything else that
accepts the plain-text exposition format — needs nothing done. Two setups need a look: a scrape
configuration that requires the OpenMetrics content type, and a check that reads a missing
# EOF as a truncated scrape. Both need their expectation relaxed.
The endpoint itself is unchanged otherwise: still /metrics (or METRICS_PATH), still enabled by
default, and still answered only for trusted addresses (METRICS_TRUSTED_IPS, 127.0.0.1 by
default) — so if nothing scrapes this service, there is nothing to do. The change arrives from the
web framework this service is built on rather than from a change of its own, carried in with the
shared libraries below.
A person's identity code now reaches every store and every token in one spelling: the identity
type, the country, a hyphen, and the national code with its separators removed —
PNOLV-XXXXXXXXXXX. The same person's card certificate may write PNOLV-XXXXXX-XXXXX and a
provider may send XXXXXX-XXXXX; compared as text those were three different people, and documents
signed under one spelling were unreachable from another.
What an integrator sees: the serial_number claim in an issued token, and the identity code the
signer-slot match compares against, are now that one spelling.
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&client_id=portal-spa&code_verifier=…{ "sub": "01J…", "serial_number": "PNOLV-XXXXXXXXXXX", "scope": "…", "tenant": "…" }Nothing is guessed. A code that already names its country keeps it — a Lithuanian person
authenticating through a Latvian provider stays Lithuanian. A code that names none is keyed under
the country of the card certificate it was read from, or the country configured for the
provider it came from. A code with no country available anywhere, or with an identity type this
platform does not recognise, refuses the login (401) and records why, rather than filing the
person under a guess: a wrong identity key is the wrong person's documents.
| Variable | Default | Purpose |
|---|---|---|
OIDC_UPSTREAM_COUNTRY |
— | Two-letter country whose register issues this provider's identity codes; used only when the claim carries no country of its own |
Set it for a generic OIDC provider whose identity-code claim is a bare national code — without it,
those logins are refused. A provider whose claim carries PNO<CC>- needs nothing: the value wins
either way. The eParaksts profile already carries LV, so eParaksts deployments need no change.
- The shared libraries moved to their current releases — the auth client at v0.21.0 and the platform kit at v1.11.2 — which carried the web framework, the HTTP stack and the JOSE library up with them. No endpoint, field, error or setting of this service changed, and no configuration needs touching. The move also clears two published advisories in the cryptography library this service depends on; a third has no fix available yet and was already present before the move, and the vulnerability scanner reports nothing this service's own code can reach.
go-platform-kit v1.11.3, go-authbyte v0.23.1, go-gdpr-audit v1.1.5 and go-sec-events
v1.2.1. No endpoint, field, error or setting changes with them, nothing in your configuration needs
touching, and this service's own behaviour is unchanged. Two of those cross a release worth naming:
go-authbyte v0.23.0 added a way to tell a natural person's identity code from an organisation's,
and go-sec-events v1.2.0 allows a security event to be emitted from work with no request behind
it — both additions to the libraries, neither a change here. The Postgres driver pgx/v5 moves to
v5.11.0 in the same pass.
Initial code.
The authbyte authorization server as first released: OAuth2/OIDC with PKCE and DPoP, Web eID card login, upstream OIDC providers (a generic discovery-driven connector plus the eParaksts profile), RFC 8693 token exchange issuing delegated service tokens, and structured audit events. Two supported modes: standalone and register-backed. AGPL-3.0-only.