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

identityharness

package
v0.0.51 Latest Latest
Warning

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

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

Documentation

Overview

Package identityharness stands in, offline, for the external parties of the agent identity credential flow: the upstream company IdP and AS-1, Mecatl's sign-in authorization server. It follows docs/drafts/agent-identity/credentials-and-lifecycle.md for every step it covers, so identity broker tests and Mecatl composition tests share one stand-in instead of each inventing its own.

The harness covers user sign-in: authorization code with PKCE at AS-1, which signs the user in at the upstream IdP as an ordinary OIDC client, and AS-1 access tokens as design step (1) describes them. Later work adds the session-open exchange, ID-JAG issuance, refresh-token revocation, broker client authentication, and a gateway stand-in, each in its own files. AS-2 is deliberately absent: tests use the real embedded ToolHive authorization server for it.

Everything runs on loopback TLS servers that close when the test ends. The harness is test code; production packages never import it.

Index

Constants

View Source
const (
	// DefaultAudience is the Mecatl API identifier AS-1 access tokens carry in
	// aud. mecak8s must accept the same value (--oidc-audience).
	DefaultAudience = "https://mecatl.test/api"
	// DefaultClientID is the public client registered when Options.Clients is
	// empty.
	DefaultClientID = "mecatui"
	// DefaultRedirectURI is DefaultClientID's only redirect URI.
	DefaultRedirectURI = "http://127.0.0.1:8765/callback"
	// DefaultAccessTokenLifetime is the AS-1 access token lifetime.
	DefaultAccessTokenLifetime = 15 * time.Minute
	// Alice and Bob are the company identities the upstream IdP knows by
	// default. Alice is the default user.
	Alice = "alice-204"
	Bob   = "bob-317"
)

Defaults used when Options leaves a field empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AS1

type AS1 struct {
	// contains filtered or unexported fields
}

AS1 stands in for AS-1, Mecatl's sign-in authorization server. It signs users in with the authorization code flow and PKCE (S256, required), sends them to the upstream IdP as an ordinary OIDC client asking for offline access, and issues the AS-1 access token design step (1) describes:

header: typ at+jwt, alg ES256, kid
claims: iss, sub, sub_id {format iss_sub, iss, sub}, aud, client_id, iat, exp, jti

sub is AS-1's own subject for the company identity: stable across sign-ins, and never shared by two company identities. Clients also receive a refresh token they can use at the refresh_token grant.

The issuer has a path (/as), as AS-1's final issuer URL does, so discovery is served both at {issuer}/.well-known/openid-configuration and at the RFC 8414 location.

func (*AS1) Audience

func (a *AS1) Audience() string

Audience is the Mecatl API identifier AS-1 access tokens carry.

func (*AS1) AuthorizationEndpoint

func (a *AS1) AuthorizationEndpoint() string

AuthorizationEndpoint is where clients start sign-in.

func (*AS1) FailNextAuthorization

func (a *AS1) FailNextAuthorization(fault Fault)

FailNextAuthorization makes the next authorization request redirect back with fault.Error instead of signing the user in.

func (*AS1) FailNextToken

func (a *AS1) FailNextToken(fault Fault)

FailNextToken makes the next token request from a registered client fail with fault.

func (*AS1) HasUpstreamRefreshToken

func (a *AS1) HasUpstreamRefreshToken(user string) bool

HasUpstreamRefreshToken reports whether AS-1 holds an upstream refresh token for user, which its later upstream re-check needs.

func (*AS1) Issuer

func (a *AS1) Issuer() string

Issuer is AS-1's issuer URL, which mecak8s accepts (--oidc-issuer).

func (*AS1) MintAccessToken

func (a *AS1) MintAccessToken(spec AccessTokenSpec) string

MintAccessToken issues an AS-1 access token without a sign-in, for tests that need a specific token or a specific defect.

func (*AS1) Subject

func (a *AS1) Subject(user string) string

Subject returns AS-1's subject for the company identity user, assigning one on first use. Two company identities never share a subject.

func (*AS1) TokenEndpoint

func (a *AS1) TokenEndpoint() string

TokenEndpoint is AS-1's token endpoint.

type AccessTokenSpec

type AccessTokenSpec struct {
	// User is the company identity (IdP subject). Empty selects the IdP's
	// default user.
	User string
	// ClientID is the client_id claim. Empty selects the first registered
	// client.
	ClientID string
	// Audience replaces the Mecatl API identifier in aud.
	Audience string
	// Issuer replaces AS-1's issuer in iss.
	Issuer string
	// IssuedAt replaces the current time in iat; exp follows from it.
	IssuedAt time.Time
	// Lifetime replaces the configured lifetime. Negative issues an expired
	// token.
	Lifetime time.Duration
	// Type replaces at+jwt in the typ header.
	Type string
	// OmitType leaves typ out of the header.
	OmitType bool
	// OmitSubID leaves the sub_id claim out.
	OmitSubID bool
	// SubID, when non-nil, replaces the sub_id claim, for a malformed one.
	SubID any
	// ForeignKey signs with a key that is not in AS-1's JWKS, under AS-1's
	// kid.
	ForeignKey bool
}

AccessTokenSpec describes an AS-1 access token for MintAccessToken. Fields left empty take the values a real sign-in produces, so a test changes only the defect it checks.

type Client

type Client struct {
	ID           string
	RedirectURIs []string
}

Client is a public OAuth client registered at AS-1, such as the TUI or Studio. Redirect URIs match exactly.

type Fault

type Fault struct {
	// Status is the HTTP status for a token endpoint fault. Zero selects 400.
	Status int
	// Error is the OAuth error code, such as access_denied or invalid_grant.
	Error string
	// Reason starts error_description, as the design's error reasons do.
	Reason string
}

Fault is a failure a stand-in returns once instead of its normal response.

type Harness

type Harness struct {
	IdP *IdP
	AS1 *AS1
	// contains filtered or unexported fields
}

Harness is a running set of stand-ins. Create it with New.

func New

func New(t testing.TB, opts Options) *Harness

New starts the stand-ins and stops them when t ends.

func (*Harness) CACertPEM

func (h *Harness) CACertPEM() []byte

CACertPEM returns the PEM certificate that verifies every stand-in.

func (*Harness) HTTPClient

func (h *Harness) HTTPClient() *http.Client

HTTPClient returns a client that trusts the stand-ins' TLS certificate and does not follow redirects.

func (*Harness) Refresh

func (h *Harness) Refresh(ctx context.Context, clientID, refreshToken string) (Tokens, error)

Refresh renews an access token with a client refresh token.

func (*Harness) SignIn

func (h *Harness) SignIn(ctx context.Context, req SignInRequest) (Tokens, error)

SignIn drives the browser side of a sign-in, as the TUI or Studio would: authorization code with PKCE at AS-1, through the upstream IdP, then the code exchange at AS-1's token endpoint. A refused sign-in returns an *OAuthError.

func (*Harness) WriteCACert

func (h *Harness) WriteCACert(t testing.TB, dir string) string

WriteCACert writes CACertPEM to a file in dir and returns its path, for a server's --oidc-ca-cert-file flag.

type IdP

type IdP struct {
	// contains filtered or unexported fields
}

IdP stands in for the upstream company IdP. AS-1 is an ordinary OIDC client of it (design step (1)): the IdP signs users in with the authorization code flow and returns an ID token naming the company identity. It issues a refresh token only when the client asks for offline access the way OIDC Core §11 requires: the offline_access scope together with prompt=consent.

Sign-in is headless. The user is the one named by login_hint, or the default user when login_hint is absent; an unknown user is refused with access_denied.

func (*IdP) AddUser

func (p *IdP) AddUser(subject string)

AddUser lets the IdP sign in subject.

func (*IdP) FailNextAuthorization

func (p *IdP) FailNextAuthorization(fault Fault)

FailNextAuthorization makes the next authorization request redirect back with fault.Error, such as access_denied, instead of signing the user in.

func (*IdP) FailNextToken

func (p *IdP) FailNextToken(fault Fault)

FailNextToken makes the next token request fail with fault.

func (*IdP) Issuer

func (p *IdP) Issuer() string

Issuer is the IdP's issuer, which AS-1 access tokens carry as sub_id.iss.

func (*IdP) RefreshTokensIssued

func (p *IdP) RefreshTokensIssued(subject string) int

RefreshTokensIssued reports how many upstream refresh tokens the IdP issued for subject, which shows whether AS-1 asked for offline access.

func (*IdP) RegisterClient

func (p *IdP) RegisterClient(id, secret, redirectURI string)

RegisterClient registers a confidential OIDC client that authenticates to the token endpoint with client_secret_basic or client_secret_post. AS-1 registers itself this way; a test hosting a real AS-1 registers that one.

type OAuthError

type OAuthError struct {
	Code        string `json:"error"`
	Description string `json:"error_description"`
}

OAuthError is an OAuth error a sign-in ended with, from the authorization redirect or the token endpoint.

func (*OAuthError) Error

func (e *OAuthError) Error() string

type Options

type Options struct {
	// Audience is the Mecatl API identifier. Empty selects DefaultAudience.
	Audience string
	// Clients are AS-1's public clients. Empty registers DefaultClientID with
	// DefaultRedirectURI.
	Clients []Client
	// Users are the company identities (IdP subjects) the upstream IdP can
	// sign in. The first is the default user. Empty selects Alice and Bob.
	Users []string
	// AccessTokenLifetime is the AS-1 access token lifetime. Zero selects
	// DefaultAccessTokenLifetime.
	AccessTokenLifetime time.Duration
	// Now is the clock for every issued token and code. Nil selects time.Now.
	Now func() time.Time
}

Options configures a Harness. The zero value is usable.

type SignInRequest

type SignInRequest struct {
	// User is the company identity to sign in. Empty selects the IdP's
	// default user.
	User string
	// ClientID is the AS-1 client. Empty selects DefaultClientID.
	ClientID string
	// RedirectURI is the client's redirect URI. Empty selects
	// DefaultRedirectURI.
	RedirectURI string
}

SignInRequest selects who signs in, with which client.

type Tokens

type Tokens struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
}

Tokens are what a client holds after signing in at AS-1.

Jump to

Keyboard shortcuts

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