Help us shape the future of Go! Take the 2026 Go Developer Survey

conformance

package
v1.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package conformance holds the shapes Socrate really produces — the claim sets of its tokens, its token, userinfo and introspection responses, and its discovery and JWKS documents — as fixtures a consumer's Socrate mock loads instead of hand-writing claims, plus the helpers to sign Socrate-shaped tokens with a test key and to check a document against a fixture.

A mock written from memory drifts from the server: a claim spelt differently, aud as a string instead of an array, a custom claim without its namespace. Such differences surface in production. The fixtures here are checked nightly against a Socrate built from ovander/go-oauth2 main (the Conformance workflow, live tests behind the conformance build tag), so a mock built on them fails in CI when the server changes, not after a deploy.

Fixtures

Names lists them. Each is a JSON document whose volatile or deployment-specific values are placeholders:

access/authorization_code                access token, authorization-code grant (password sign-in)
access/authorization_code_mfa            the same after a TOTP or recovery code: amr ["pwd","otp","mfa"], acr "mfa"
access/refresh                           access token from the refresh_token grant (auth_time, amr, acr kept)
access/audience_dual                     user token, AUDIENCE_MODE=dual, client with a registered audience
access/custom_claims                     user token of a client with claim mappings (namespaced custom claims)
access/client_credentials                service-account token: sub "app:<id>", scope "api" by default
access/client_credentials_audience_dual  the same, AUDIENCE_MODE=dual, client with a registered audience
access/client_credentials_custom_claims  the same, client with claim mappings (app and literal sources only)
access/client_credentials_dpop           the same, requested with a DPoP proof: cnf.jkt
id_token/authorization_code              ID token (nonce echoed), and …_mfa, refresh (no nonce), custom_claims
refresh_token/authorization_code         refresh token (Socrate's are JWTs; treat them as opaque)
token_response/authorization_code        /oauth/token response, and …/refresh, …/client_credentials
userinfo                                 /oauth/userinfo response
introspection/access                     /oauth/introspect, active user token, and …/client_credentials, …/inactive
discovery                                /.well-known/openid-configuration with DPOP_MODE=off (the default)
discovery_dpop                           the same with DPoP enabled (dpop_signing_alg_values_supported)
jwks                                     /.well-known/jwks.json (one entry per signing key)
jose_header                              the JOSE header of every Socrate token

Without a registered audience, or with AUDIENCE_MODE=off (the server default), aud is ["<client_id>"]: the plain fixtures. Socrate's audience mode, claim namespace and DPoP mode are server-wide settings; the fixtures name the one they assume.

Placeholders

A string value that is a whole placeholder stands for one value; Load fills it from Params (or a default), and Match checks it by rule:

<issuer>          Params.Issuer — also inside strings: "<issuer>/oauth/token"
<client_id>       Params.ClientID
<audience>        in an array only: Params.Audiences, appended after the client_id
<sub>             Params.Subject: the user's numeric id as a decimal string
<app_sub>         "app:" + Params.AppID
<role>            Params.Role, the user's role in the client
<app_roles>       {client_id: role} — the user's role in every client they belong to
<email>, <name>   Params.Email, Params.Name
<nonce>           Params.Nonce, echoed from the authorization request
<tenant_id>       Params.TenantID (a UUID) — the value of a literal claim mapping
<attribute:K>     Params.Attributes[K] — a user attribute projected by a claim mapping
<scope:D>         Params.Scope, or D, the scope Socrate grants when none is asked for
<iat>, <nbf>      Params.Now, as seconds since the epoch
<auth_time>       Params.AuthTime (default Params.Now)
<exp>             Params.Now + Params.TTL; <refresh_exp>: + Params.RefreshTTL
<expires_in>      Params.TTL in seconds
<jti>, <kid>      a UUID (kid: the signing key's ID)
<token_version>   Params.TokenVersion
<at_hash>         the OIDC at_hash of Params.AccessToken (random without it)
<jkt>             Params.JKT, a JWK SHA-256 thumbprint
<access_token>, <refresh_token>, <id_token>   Params.AccessToken, …
<n>               the RSA modulus of each key in Params.Keys

A key may contain <ns>, the claim namespace (Params.ClaimsNamespace, default "https://socrate/", Socrate's CLAIMS_NAMESPACE default): "<ns>tenant_id" is the claim "https://socrate/tenant_id".

Using the fixtures in a mock

key, _ := conformance.NewKey()
jwks, _ := conformance.JWKS(key)                       // serve at /.well-known/jwks.json
disc, _ := conformance.Discovery(srv.URL)              // serve at /.well-known/openid-configuration
tok, _ := conformance.Sign("access/authorization_code", conformance.Params{
	Issuer: srv.URL, ClientID: "my-app", Subject: "42",
}, key)

Match checks a document — a token's claims, a response body — against a fixture: same keys, nothing missing or extra, and every value equal to the fixture's or satisfying its placeholder's rule. A placeholder whose Params field is set must equal it exactly.

Live checks

Behind the conformance build tag, this package's tests obtain the real documents from a running Socrate over raw HTTP (hosted sign-in and consent, TOTP enrolment, a DPoP proof) and Match every fixture against them; the jwtauth, socrate, bff and pep packages each test themselves against the same server. They read SOCRATE_ISSUER, SOCRATE_ADMIN_URL, SOCRATE_REDIRECT_URI, SOCRATE_AUDIENCE, SOCRATE_TENANT_ID, SOCRATE_USER_PASSWORD, SOCRATE_POLICY_MODE and, for each of the plain, dual and claims clients, SOCRATE_<PLAIN|DUAL|CLAIMS>_CLIENT_ID and _SECRET, and expect a server with AUDIENCE_MODE=dual and DPOP_MODE=observe. Every fixture is matched against the live server except discovery, which a unit test ties to the live-checked discovery_dpop. scripts/conformance-local.sh builds, starts and seeds a Socrate and runs them; the Conformance workflow does the same nightly.

Example (Mock)

A Socrate mock for a resource server's tests: its JWKS and discovery documents, and access tokens with the claims Socrate really issues.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/ovander/backendkit/conformance"
)

func main() {
	key, err := conformance.NewKey()
	if err != nil {
		panic(err)
	}
	mux := http.NewServeMux()
	srv := httptest.NewServer(mux)
	defer srv.Close()

	serve := func(doc map[string]any) http.HandlerFunc {
		return func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(doc)
		}
	}
	jwks, _ := conformance.JWKS(key)
	disc, _ := conformance.Discovery(srv.URL)
	mux.Handle("/.well-known/jwks.json", serve(jwks))
	mux.Handle("/.well-known/openid-configuration", serve(disc))

	// A user's access token for the client "my-app"…
	userToken, _ := conformance.Sign("access/authorization_code", conformance.Params{
		Issuer: srv.URL, ClientID: "my-app", Subject: "42", Role: "editor",
	}, key)
	// …and a service account's, carrying a tenant through a literal claim mapping.
	serviceToken, _ := conformance.Sign("access/client_credentials_custom_claims", conformance.Params{
		Issuer: srv.URL, ClientID: "billing-worker", AppID: 12,
		TenantID: "5b0c7a52-3e0c-4d2a-9f8e-0d6f1a2b3c4d",
	}, key)

	fmt.Println(userToken != "" && serviceToken != "")
}
Output:
true

Index

Examples

Constants

View Source
const (
	DefaultIssuer          = "https://socrate.example.com"
	DefaultClientID        = "my-app"
	DefaultAudience        = "https://api.example.com"
	DefaultSubject         = "42"
	DefaultAppID           = 7
	DefaultRole            = "user"
	DefaultEmail           = "user@example.com"
	DefaultName            = "Example User"
	DefaultTenantID        = "00000000-0000-4000-8000-000000000001"
	DefaultClaimsNamespace = "https://socrate/"
	DefaultTTL             = 15 * time.Minute
	DefaultRefreshTTL      = 7 * 24 * time.Hour
)

Defaults Load uses for an unset Params field.

Variables

This section is empty.

Functions

func Discovery

func Discovery(issuer string) (map[string]any, error)

Discovery returns the discovery document of a Socrate whose issuer is issuer (DPoP off, the server default; load "discovery_dpop" for the other).

func JWKS

func JWKS(keys ...Key) (map[string]any, error)

JWKS returns the JWKS document Socrate serves for keys.

func Load

func Load(name string, p Params) (map[string]any, error)

Load returns the fixture with every placeholder resolved from p. The result is a fresh map the caller may change, e.g. to add a claim of its own.

Example
package main

import (
	"fmt"

	"github.com/ovander/backendkit/conformance"
)

func main() {
	claims, err := conformance.Load("access/client_credentials", conformance.Params{
		Issuer: "https://socrate.example.com", ClientID: "billing-worker", AppID: 12,
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(claims["sub"], claims["aud"], claims["scope"], claims["type"])
}
Output:
app:12 [billing-worker] api access

func Match

func Match(name string, got any, p Params) error

Match checks got — a token's claims, a response body, a document — against the fixture: the same keys, none missing and none extra, every literal value equal, and every placeholder satisfied by its rule (a UUID jti, a numeric sub, an integer exp after iat, …). A placeholder whose Params field is set must equal it exactly; <ns> keys use Params.ClaimsNamespace or the default. got may be any value that marshals to a JSON object (jwt.MapClaims, a struct, a map). The error lists every difference found.

Example
package main

import (
	"fmt"

	"github.com/ovander/backendkit/conformance"
)

func main() {
	// A hand-written mock token: aud as a bare string, no type, no jti.
	handWritten := map[string]any{
		"iss": "https://socrate.example.com", "sub": "42", "aud": "my-app",
		"iat": 1700000000, "exp": 1700000900,
	}
	err := conformance.Match("access/client_credentials", handWritten, conformance.Params{})
	fmt.Println(err != nil)
}
Output:
true

func Names

func Names() []string

Names returns the name of every fixture, sorted, for example "access/authorization_code" or "discovery".

func Raw

func Raw(name string) ([]byte, error)

Raw returns the fixture's JSON with its placeholders unresolved.

func Sign

func Sign(name string, p Params, key Key) (string, error)

Sign returns the fixture — an access/, id_token/ or refresh_token/ claim set, resolved from p — as a compact JWS signed RS256 with key, under Socrate's JOSE header (alg, kid, typ). A resource server validating it against JWKS(key) sees exactly what it would see from Socrate.

Types

type Key

type Key struct {
	ID      string
	Private *rsa.PrivateKey
}

Key is an RSA signing key and the kid Socrate would publish it under.

func NewKey

func NewKey() (Key, error)

NewKey returns a fresh 2048-bit RSA key with a UUID kid, as Socrate's key ring generates them.

type Params

type Params struct {
	Issuer    string   // <issuer>, without a trailing slash
	ClientID  string   // <client_id>
	Audiences []string // <audience>: the client's registered audiences (AUDIENCE_MODE=dual)
	Subject   string   // <sub>: the user's numeric id
	AppID     uint     // <app_sub> is "app:<AppID>"
	Role      string   // <role> and the client's entry in <app_roles>
	Email     string   // <email>
	Name      string   // <name>
	Scope     string   // <scope:…>; empty uses the fixture's default
	Nonce     string   // <nonce>
	TenantID  string   // <tenant_id>
	// Attributes holds the user attributes <attribute:K> projects; Load's
	// default is {"department": "engineering"}.
	Attributes      map[string]any
	ClaimsNamespace string // <ns>; default DefaultClaimsNamespace
	TokenVersion    int    // <token_version>; default 1
	JKT             string // <jkt>

	Now        time.Time     // <iat>, <nbf>; default time.Now()
	AuthTime   time.Time     // <auth_time>; default Now
	TTL        time.Duration // <exp> = Now+TTL, <expires_in>; default DefaultTTL
	RefreshTTL time.Duration // <refresh_exp> = Now+RefreshTTL; default DefaultRefreshTTL

	// AccessToken, RefreshToken and IDToken fill the token_response fixtures;
	// AccessToken also gives an ID token its real at_hash.
	AccessToken  string
	RefreshToken string
	IDToken      string

	// Keys fill the jwks fixture (one entry each) and <kid>.
	Keys []Key
}

Params fills a fixture's placeholders (Load, Sign) and, when set, pins them to exact values (Match). Every field is optional: Load falls back to the Default… constants (a random nonce, jti and jkt; time.Now), and Match checks an unset field by its placeholder's rule only.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL