axiam

package module
v1.0.0-alpha15 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

axiam SDK (Go)

CI Coverage Status Go Reference Go Report Card License

Official Go client SDK for AXIAM — Access eXtended Identity and Authorization Management.

Package identity

Contract conformance

This SDK conforms to CONTRACT.md §1–§11 (including §6.1 mTLS).

See CONTRACT.md for the full cross-language behavioral contract.

Status

Implemented (Phase 18). REST client (login/MFA/refresh/logout, authz check/can/batch-check), gRPC client, AMQP consumer with HMAC verification, local JWKS verification, and net/http middleware are all available. Five runnable examples live under examples/.

Installation

go get github.com/ilpanich/axiam-go-sdk@latest

Or pin an explicit release:

go get github.com/ilpanich/axiam-go-sdk@vX.Y.Z
import axiam "github.com/ilpanich/axiam-go-sdk"

Usage

Login + MFA (§1, §5)
// tenantSlug is required — no default tenant (§5). Login and Refresh also
// require organization context (§5.1) — a tenant slug is only unique within
// an organization — so pass the org via WithOrgSlug (or WithOrgID for a UUID);
// a login without it is rejected with 400 "must provide org_id or org_slug".
client, err := axiam.NewClient(baseURL, tenantSlug, axiam.WithOrgSlug(orgSlug))
if err != nil {
	// handle error
}

result, err := client.Login(ctx, email, password)
if err != nil {
	// handle error
}
if result.MFARequired {
	completed, err := client.VerifyMfa(ctx, result.MFAToken, totpCode)
	// ...
}

See examples/login-mfa.

REST authorization checks — CheckAccess / Can / BatchCheck (§1)
allowed, reason, err := client.CheckAccess(ctx, "resource:read", resourceID)
canWrite, err := client.Can(ctx, "resource:write", resourceID)
results, err := client.BatchCheck(ctx, []axiam.AccessCheck{
	{Action: "resource:read", ResourceID: resourceID},
})

See examples/authz-check.

gRPC authorization checks (§1, §5, §9)
creds, err := axiamgrpc.NewTLSCredentials(nil, nil, nil) // strict TLS; arg 1 is an optional custom CA PEM for dev servers (§6)
conn, err := axiamgrpc.NewGRPCClient(target, creds, interceptor)
authzClient := axiamgrpc.NewAuthzClient(conn, refreshFn)

allowed, denyReason, err := authzClient.CheckAccess(ctx, axiamgrpc.CheckAccessRequest{
	TenantID: tenantID, SubjectID: subjectID, Action: "resource:read", ResourceID: resourceID,
})

See examples/grpc-checkaccess.

mTLS / client certificates (§6.1)

AXIAM can authenticate IoT devices and service accounts by mutual TLS: the client presents an X.509 identity certificate (signed by the tenant's organization CA) that the server binds to a service account. Configure the client identity with WithClientCertificate — it is applied to both the REST and gRPC transports of the same logical client, and it never relaxes server verification (it is additive to WithCustomCA/§6, and the TLS-1.3 floor and strict RootCAs behavior are unchanged).

// PEM cert chain + PEM private key (PKCS#8 or PKCS#1).
client, err := axiam.NewClient(baseURL, tenantSlug,
	axiam.WithCustomCA(serverCAPEM),                 // trust the server's CA (§6)
	axiam.WithClientCertificate(certPEM, keyPEM),    // present our identity (§6.1)
)

// The same identity over gRPC — pass the SAME cert chain + key:
creds, err := axiamgrpc.NewTLSCredentials(serverCAPEM, certPEM, keyPEM)

mTLS is opt-in: omitting WithClientCertificate leaves the default bearer-cookie behavior unchanged. The private key is secret material (§7) — it is held behind the SDK's Sensitive type and never appears in any log, error, or display output, and there is no public getter for it.

AMQP consumer with HMAC verification (§8)
handler := func(ctx context.Context, event amqp.Event) error {
	// process event.Fields — hmac_signature has already been verified and removed
	return nil // Ack; return amqp.ErrDrop for a poison message (Nack, no requeue)
}
err := amqp.Consume(ctx, ch, queue, signingKey, handler)

See examples/amqp-consumer.

net/http middleware (§10)
verifier, err := axiam.NewJWKSVerifier(ctx, baseURL, nil)
guarded := middleware.Middleware(verifier, tenantSlug)(mux)

// inside a handler:
user, ok := middleware.UserFromContext(r.Context())

See examples/middleware-guard.

Declarative authorization helpers (§11)

On top of the §10 Middleware guard, middleware.RequireAuth, middleware.RequireAccess, and middleware.RequireRole add a per-route authorization layer (CONTRACT.md §11). Go has no macro/annotation/decorator facility, so these are per-route http.Handler wrappers under the same canonical require_auth / require_access / require_role vocabulary every other AXIAM SDK uses. They run strictly after the §10 guard — they never extract or verify a token themselves, only consuming the identity Middleware already injected — and they perform no decision caching: every request re-checks.

verifier, err := axiam.NewJWKSVerifier(ctx, baseURL, nil)
client, err := axiam.NewClient(baseURL, tenantSlug) // *axiam.Client satisfies middleware.AccessChecker

mux := http.NewServeMux()

// GET /docs/{id} requires the authenticated caller to pass a
// "documents:read" check for the {id} resolved from the path.
mux.Handle("/docs/{id}", middleware.RequireAccess(
	client, "documents:read", middleware.ResourceFromPath("id"),
)(docHandler))

// A route that only needs an authenticated identity, no resource check.
mux.Handle("/whoami", middleware.RequireAuth()(whoamiHandler))

// A cheap, LOCAL role check — no server round-trip, and NOT a substitute
// for RequireAccess's resource-level check.
mux.Handle("/admin", middleware.RequireRole("admin")(adminHandler))

guarded := middleware.Middleware(verifier, tenantSlug)(mux) // §10 guard wraps the whole mux

The check is always made for the request's authenticated user (subject_id), never the application's own client session — this is why RequireAccess takes a middleware.AccessChecker (satisfied by *axiam.Client's additive CheckAccessAs method) rather than reusing CheckAccess directly. A resource id that can't be resolved (missing path value, empty StaticResource, or a failing custom ResourceResolver) is a 400, never a silent allow. A transport failure while calling the authz endpoint fails closed with 503 — it is never treated as an allow.

See examples/middleware-guard (the GET /docs/{id} route).

Versioning

Releases are tagged vX.Y.Z. Pushing such a tag triggers the module-publish CI job, which verifies the tag was cut from main and asks proxy.golang.org to fetch it; pull-request events never trigger publish.

There is no registry upload step — for Go, the git tag is the release, and go get resolves it through the module proxy. API docs appear automatically on pkg.go.dev once the proxy has seen the tag.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAuth    = errors.New("axiam: authentication error")
	ErrAuthz   = errors.New("axiam: authorization error")
	ErrNetwork = errors.New("axiam: network error")
)

Sentinel errors for errors.Is-based discrimination convenience (CONTRACT.md §2, D-04). These are never returned directly — only *AuthError/*AuthzError/*NetworkError instances are, each of which implements Is(target) to match the corresponding sentinel.

Functions

This section is empty.

Types

type AccessCheck

type AccessCheck struct {
	Action     string `json:"action"`
	ResourceID string `json:"resource_id"`
	Scope      string `json:"scope,omitempty"`
	// SubjectID is optional and, when set, asks the server to evaluate the
	// check for this subject rather than the caller's own session
	// (CONTRACT.md §11.2 — declarative authorization helpers pass the
	// request's authenticated user_id here so the check runs for the end
	// user, not the application's own service-account session). Omitted
	// from the wire payload when empty, preserving today's request shape
	// for CheckAccess/Can/BatchCheck callers that never set it.
	SubjectID string `json:"subject_id,omitempty"`
}

AccessCheck is a single access check request (CONTRACT.md §1). ResourceID is a string (server-side UUID) rather than a typed UUID so callers can pass either a UUID string or, in future, other resource-id encodings without a breaking type change; the server is the source of truth for validation.

type AccessResult

type AccessResult struct {
	Allowed bool   `json:"allowed"`
	Reason  string `json:"reason,omitempty"`
}

AccessResult is the outcome of a single access check (mirrors CheckAccessResponse).

type AuthError

type AuthError struct {
	Message string
}

AuthError represents an authentication failure: wrong credentials, expired session, MFA failure, or a 401 on refresh (CONTRACT.md §2).

func (*AuthError) Error

func (e *AuthError) Error() string

func (*AuthError) Is

func (e *AuthError) Is(target error) bool

Is reports whether target is the ErrAuth sentinel, enabling errors.Is(err, ErrAuth) to match any *AuthError.

type AuthzError

type AuthzError struct {
	Message    string
	Action     string
	ResourceID string
}

AuthzError represents an authorization failure: the caller is authenticated but lacks permission for the requested operation (CONTRACT.md §2). Action/ResourceID are optional and populated when known from the response body.

func (*AuthzError) Error

func (e *AuthzError) Error() string

func (*AuthzError) Is

func (e *AuthzError) Is(target error) bool

Is reports whether target is the ErrAuthz sentinel, enabling errors.Is(err, ErrAuthz) to match any *AuthzError.

type Client

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

Client is the AXIAM SDK's REST entry point (CONTRACT.md §1-§10). See NewClient.

func NewClient

func NewClient(baseURL, tenantSlug string, opts ...Option) (*Client, error)

NewClient constructs a Client. baseURL and tenantSlug are positional and required (D-03): an empty tenantSlug returns an *AuthError — AXIAM is multi-tenant and there is no default tenant, so this can never be a silent default (CONTRACT.md §5, SC#1).

The returned Client always owns a per-instance cookiejar and a TLS-1.3-minimum transport; WithHTTPClient may override the Transport/timeout, but the SDK re-applies its own jar and TLS config over any supplied client afterward (D-09) so neither can be silently dropped or bypassed.

func (*Client) BatchCheck

func (c *Client) BatchCheck(ctx context.Context, reqs []AccessCheck) ([]AccessResult, error)

BatchCheck performs POST /api/v1/authz/check/batch (CONTRACT.md §1), evaluating an ordered list of checks; results are returned in the same order as reqs. Eligible for CF-01's bounded retry (read-only).

func (*Client) Can

func (c *Client) Can(ctx context.Context, action, resourceID string, scope ...string) (bool, error)

Can is an alias for CheckAccess targeting browser/UI scenarios (CONTRACT.md §1 note) — returns only the allowed boolean.

func (*Client) CheckAccess

func (c *Client) CheckAccess(ctx context.Context, action, resourceID string, scope ...string) (bool, string, error)

CheckAccess performs POST /api/v1/authz/check (CONTRACT.md §1), evaluating a single authorization check for the given action/ resourceID/scope. This is a read-only, idempotent operation eligible for CF-01's bounded retry on transient NetworkError.

func (*Client) CheckAccessAs

func (c *Client) CheckAccessAs(ctx context.Context, subjectID, action, resourceID string, scope ...string) (bool, string, error)

CheckAccessAs performs POST /api/v1/authz/check (CONTRACT.md §1) on behalf of subjectID rather than this Client's own session (CONTRACT.md §11.2). This is additive alongside CheckAccess — existing callers/signatures are unchanged — and exists specifically so declarative authorization helpers (middleware.RequireAccess) can evaluate the check for the request's authenticated user_id instead of the application's own (typically service-account) session. A blank subjectID behaves exactly like CheckAccess (the subject_id field is omitted from the wire request).

func (*Client) Login

func (c *Client) Login(ctx context.Context, email, password string) (LoginResult, error)

Login performs POST /api/v1/auth/login (CONTRACT.md §1). On success (no MFA), tokens are already present in the cookie jar and the org_id claim has been resolved+cached. When the server signals MFA is required, returns LoginResult{MFARequired: true, ...} — this is an expected outcome, not an error.

func (*Client) Logout

func (c *Client) Logout(ctx context.Context) error

Logout performs POST /api/v1/auth/logout (CONTRACT.md §1) and clears in-memory token state.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context) error

Refresh performs POST /api/v1/auth/refresh (CONTRACT.md §1), routed through the sync.Mutex single-flight guard (§9) so concurrent 401s share exactly one in-flight refresh call. A 401 on the refresh call itself is AuthError with no retry (§9.3).

func (*Client) VerifyMfa

func (c *Client) VerifyMfa(ctx context.Context, mfaToken Sensitive, code string) (LoginResult, error)

VerifyMfa performs POST /api/v1/auth/mfa/verify (CONTRACT.md §1), completing the two-phase flow started by Login when MFARequired was true.

type JWKSVerifier

type JWKSVerifier = jwks.Verifier

JWKSVerifier is the public entry point for this SDK's local JWKS verification primitive (CONTRACT.md §10, D-06) — the shared local-verify mechanism consumed by the net/http middleware (package middleware). It is a thin re-export of the internal jwks.Verifier so callers outside this module never need to import an internal/ package directly.

IMPORTANT: JWKSVerifier.Verify validates the token SIGNATURE ONLY — it does NOT check expiry. Callers using this type directly (rather than via middleware.Middleware, which checks expiry for you) MUST compare the returned Claims.Exp against time.Now().Unix() before trusting the token (WR-03).

func NewJWKSVerifier

func NewJWKSVerifier(ctx context.Context, baseURL string, hc *http.Client) (*JWKSVerifier, error)

NewJWKSVerifier constructs a JWKSVerifier bound to {baseURL}/oauth2/jwks (trailing slash on baseURL trimmed before joining). hc may be nil, in which case a default *http.Client is used. The cache is registered but not eagerly populated; the first Verify call triggers the initial fetch.

This is the exported constructor middleware.Middleware examples wire against — see examples/middleware-guard.

type LoginResult

type LoginResult struct {
	// MFARequired is true when the server responded with an MFA challenge
	// instead of a completed session; call VerifyMfa next with MFAToken.
	MFARequired bool
	// MFAToken carries the opaque challenge token when MFARequired is
	// true. Treated as sensitive (short-lived bearer of "logging in as
	// this user").
	MFAToken Sensitive
	// AvailableMethods lists MFA methods available to satisfy the
	// challenge (only populated when MFARequired is true).
	AvailableMethods []string
	// SessionID is the server-issued session id (only populated on a
	// completed, non-MFA-pending login/verify_mfa).
	SessionID string
	// ExpiresIn is the access token lifetime in seconds, as reported by
	// the server (only populated on a completed login/verify_mfa).
	ExpiresIn uint64
}

LoginResult is the outcome of Login/VerifyMfa (CF-04). MFA required is an expected outcome, not an error: check MFARequired before assuming the session is established.

type NetworkError

type NetworkError struct {
	Message string
	// contains filtered or unexported fields
}

NetworkError represents a transport-level failure: connection refused, timeout, TLS error, DNS failure, or a server-side 5xx (CONTRACT.md §2).

cause is unexported and MUST only ever be populated via newNetworkError, which redacts sensitive headers from any wrapped *http.Response BEFORE constructing the error (D-04, Phase 17 CR-04 carry-forward) — never construct a NetworkError directly from an unredacted *http.Response.

func (*NetworkError) Error

func (e *NetworkError) Error() string

func (*NetworkError) Is

func (e *NetworkError) Is(target error) bool

Is reports whether target is the ErrNetwork sentinel, enabling errors.Is(err, ErrNetwork) to match any *NetworkError.

func (*NetworkError) Unwrap

func (e *NetworkError) Unwrap() error

Unwrap exposes the underlying (already-redacted) cause for errors.Is/As and errors.Unwrap chains.

type Option

type Option func(*clientConfig)

Option configures a Client at construction time (D-03).

func WithClientCertificate

func WithClientCertificate(certPEM, keyPEM []byte) Option

WithClientCertificate configures a client-certificate identity for mutual TLS (CONTRACT.md §6.1). certPEM is a PEM-encoded X.509 certificate chain and keyPEM is the matching PEM-encoded private key (PKCS#8 or PKCS#1). The SDK presents this identity on BOTH the REST transport (here) and any gRPC channel built for the same logical client (grpc.NewTLSCredentials).

Presenting a client certificate NEVER relaxes server verification: this is additive to WithCustomCA/§6 and keeps the SDK's TLS-1.3 floor and strict RootCAs behavior unchanged. A non-PEM cert/key pair is a construction-time error returned from NewClient, consistent with WithCustomCA.

The private key is secret material (§7): it is held behind the SDK's Sensitive type and never appears in any log, error, or display output.

func WithCustomCA

func WithCustomCA(pem []byte) Option

WithCustomCA adds a PEM-encoded CA certificate to the TLS verification chain (§6). This is the ONLY TLS-related escape hatch — there is no option anywhere in this SDK that disables or weakens certificate verification. Returns a construction-time error via NewClient if pem is not valid PEM.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient supplies a base *http.Client whose Transport/Timeout the SDK adopts. D-09: the SDK ALWAYS re-applies its own cookiejar and TLS config over the supplied client afterward — an override can never silently drop the jar (breaking every post-login request) or bypass TLS verification.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger supplies an injectable, redaction-aware logger (CF-02). OFF by default (nil logger — the SDK never logs unless a logger is supplied). The SDK never emits raw token values regardless of the logger's configured level (Sensitive redacts itself in any log call).

func WithOrgID

func WithOrgID(id uuid.UUID) Option

WithOrgID sets the organization UUID the real login/refresh endpoints require (RESEARCH.md Pitfall 3). Mutually exclusive with WithOrgSlug — last call wins.

func WithOrgSlug

func WithOrgSlug(slug string) Option

WithOrgSlug sets the organization slug the real login/refresh endpoints require (RESEARCH.md Pitfall 3). Mutually exclusive with WithOrgID — last call wins.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout overrides the default request timeout applied to the SDK's http.Client (CF-03; default 30s).

type Sensitive

type Sensitive string

Sensitive wraps a token-carrying string so it can never accidentally leak via fmt verbs, Go-syntax representation, or JSON encoding (CONTRACT.md §7, D-08). All token-carrying fields (access token, refresh token, MFA challenge token, AMQP signing key) MUST use this type.

The raw value is reachable only via the package-internal expose() accessor — Sensitive deliberately has no public getter.

func (Sensitive) Format

func (Sensitive) Format(f fmt.State, verb rune)

Format implements fmt.Formatter, closing the fmt-verb leak path (%v/%+v/%s/%q/width/precision) that a bare String() method does not fully cover — this is the CR-04 leak class this type exists to prevent.

func (Sensitive) GoString

func (Sensitive) GoString() string

GoString implements fmt.GoStringer, covering %#v (Go-syntax representation), which bypasses String()/Format() entirely if not implemented.

func (Sensitive) MarshalJSON

func (Sensitive) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler so any struct embedding a Sensitive field serializes the redacted placeholder rather than the raw value.

func (Sensitive) String

func (Sensitive) String() string

String implements fmt.Stringer. Covers direct String() calls and the default fmt verb behavior for types without a more specific Format/GoString override (String() alone would still leak on %#v without GoString below).

Directories

Path Synopsis
Package amqp implements the AXIAM AMQP event consumer: a closure-handler Consume loop that HMAC-SHA256-verifies every delivery BEFORE the caller's handler ever runs (CONTRACT.md §8, D-07, SC#4).
Package amqp implements the AXIAM AMQP event consumer: a closure-handler Consume loop that HMAC-SHA256-verifies every delivery BEFORE the caller's handler ever runs (CONTRACT.md §8, D-07, SC#4).
examples
amqp-consumer command
Command amqp-consumer demonstrates amqp.Consume with a closure handler that shows the full ack/nack matrix (CONTRACT.md §8, D-07).
Command amqp-consumer demonstrates amqp.Consume with a closure handler that shows the full ack/nack matrix (CONTRACT.md §8, D-07).
authz-check command
Command authz-check demonstrates the REST authorization surface: CheckAccess, Can (the browser/UI alias), and BatchCheck (CONTRACT.md §1).
Command authz-check demonstrates the REST authorization surface: CheckAccess, Can (the browser/UI alias), and BatchCheck (CONTRACT.md §1).
grpc-checkaccess command
Command grpc-checkaccess demonstrates the gRPC authorization transport: CheckAccess and BatchCheck over a lazily-connected *grpc.ClientConn (CONTRACT.md §1, §5, §9).
Command grpc-checkaccess demonstrates the gRPC authorization transport: CheckAccess and BatchCheck over a lazily-connected *grpc.ClientConn (CONTRACT.md §1, §5, §9).
login-mfa command
Command login-mfa demonstrates the two-phase Login/VerifyMfa flow (CONTRACT.md §1, §5).
Command login-mfa demonstrates the two-phase Login/VerifyMfa flow (CONTRACT.md §1, §5).
middleware-guard command
Command middleware-guard demonstrates wrapping a sample net/http route with middleware.Middleware (CONTRACT.md §10, SC#1), plus a second route additionally protected with middleware.RequireAccess (CONTRACT.md §11 declarative authorization helpers).
Command middleware-guard demonstrates wrapping a sample net/http route with middleware.Middleware (CONTRACT.md §10, SC#1), plus a second route additionally protected with middleware.RequireAccess (CONTRACT.md §11 declarative authorization helpers).
Package grpc implements the gRPC transport for AuthorizationService (CheckAccess/BatchCheckAccess) with strict TLS and a sync-safe auth/tenant interceptor (CONTRACT.md §5/§6, SC#3).
Package grpc implements the gRPC transport for AuthorizationService (CheckAccess/BatchCheckAccess) with strict TLS and a sync-safe auth/tenant interceptor (CONTRACT.md §5/§6, SC#3).
internal
jwks
Package jwks implements local JWKS fetch/cache/verification via lestrrat-go/jwx/v3 (D-06/§10), the shared local-verify primitive consumed by the net/http middleware (Plan 05) and any proactive-refresh check.
Package jwks implements local JWKS fetch/cache/verification via lestrrat-go/jwx/v3 (D-06/§10), the shared local-verify primitive consumed by the net/http middleware (Plan 05) and any proactive-refresh check.
refreshguard
Package refreshguard implements the sync.Mutex single-flight refresh guard required by CONTRACT.md §9: exactly one in-flight POST /api/v1/auth/refresh call across any number of concurrent callers observing the same expired access token, with a double-check-after-lock pattern and no retry loop on failure (§9.3).
Package refreshguard implements the sync.Mutex single-flight refresh guard required by CONTRACT.md §9: exactly one in-flight POST /api/v1/auth/refresh call across any number of concurrent callers observing the same expired access token, with a double-check-after-lock pattern and no retry loop on failure (§9.3).
Package middleware implements the net/http middleware / route-guard interface (CONTRACT.md §10, D-06).
Package middleware implements the net/http middleware / route-guard interface (CONTRACT.md §10, D-06).

Jump to

Keyboard shortcuts

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