Documentation
¶
Overview ¶
Package authn defines the transport-neutral, pluggable AUTHENTICATION seams for devedge services — the "verify" and "mint" halves of the two-tier token model (WS-026). It is deliberately free of any JOSE/JWKS/OIDC-library types; the concrete signing/verifying backend lives in the nested authn/oidc module so the SDK root stays dependency-light.
The two-tier model ¶
Authentication answers "who is the verified caller?" and produces an authz.Principal; authorization (authz.Authorizer) then decides what that principal may do. authn never authorizes — it only verifies identity and authors the claims that become the principal.
Three cooperating roles:
- Role 1 — the upstream IdP (a separate service, e.g. devedge-idp) issues an IDENTITY ASSERTION (an OIDC id_token) with COARSE claims only: identity + app-access entitlement. It does not mint API bearers.
- Role 2 — the app identity is a confidential relying party that completes the OIDC dance with the IdP, then MINTS + signs its own app bearer via an Issuer, authoring the rich app-specific claims through a ClaimsMapper.
- Role 3 — the microservice VERIFIES the app bearer via an Authenticator (signature + iss/aud/exp) and maps its claims back to an authz.Principal.
Topology ¶
TokenTopology selects who mints and whom the verifier trusts. The verify seam is topology-agnostic — it verifies a bearer against whichever issuer it is configured to trust — so single-issuer is just Role 1 + Role 3 (minting off) and two-tier adds the Role 2 minter. The default is TwoTier.
Index ¶
- Constants
- Variables
- func NewRoundTripper(base http.RoundTripper, ts TokenSource, audience string) http.RoundTripper
- func UnaryClientInterceptor(ts TokenSource, r AudienceResolver) grpc.UnaryClientInterceptor
- func UnaryServerInterceptor(a Authenticator, opts ...InterceptorOption) grpc.UnaryServerInterceptor
- func VerifiedPrincipal(ctx context.Context) (authz.Principal, error)
- type AudienceResolver
- type Authenticator
- type AuthenticatorFunc
- type ClaimsMapper
- type Identity
- type InterceptorOption
- type Issuer
- type StaticAudiences
- type StaticClaimsMapper
- type StaticClaimsOption
- type TokenSource
- type TokenTopology
Constants ¶
const DefaultMetadataKey = "authorization"
DefaultMetadataKey is the incoming metadata key the bearer is read from.
Variables ¶
var ErrNoClaims = errors.New("authn: no authored claims for identity in this app")
ErrNoClaims is returned by a ClaimsMapper when an identity is entitled but has no authored claims for the app and the mapper is configured to require them.
var ErrNotEntitled = errors.New("authn: identity not entitled to this app")
ErrNotEntitled is returned by a ClaimsMapper when an identity is not entitled to enter the app (its app-access set does not include the app).
Functions ¶
func NewRoundTripper ¶ added in v0.56.0
func NewRoundTripper(base http.RoundTripper, ts TokenSource, audience string) http.RoundTripper
NewRoundTripper wraps base with one that attaches "Authorization: Bearer <token>" for a single known audience — for REST clients that each talk to one target. It obtains the token from ts per request (so caching/exchange happen in ts). A transport error obtaining the token fails the request; it never sends the request without the intended token. base nil defaults to http.DefaultTransport.
func UnaryClientInterceptor ¶ added in v0.56.0
func UnaryClientInterceptor(ts TokenSource, r AudienceResolver) grpc.UnaryClientInterceptor
UnaryClientInterceptor returns a gRPC client interceptor that attaches an outbound bearer for the target service. It resolves the target audience from the invoked method's service (the "/pkg.Service/Method" prefix) via r, obtains a token for that audience from ts, and sets "authorization: Bearer <token>" on the outgoing metadata. It is FAIL-CLOSED: a method whose service has no mapped audience returns codes.FailedPrecondition without making the call, so the raw inbound token is never sent cross-domain by accident.
func UnaryServerInterceptor ¶
func UnaryServerInterceptor(a Authenticator, opts ...InterceptorOption) grpc.UnaryServerInterceptor
UnaryServerInterceptor returns the authentication interceptor (Role 3). It runs BEFORE the authz interceptor: it pulls the bearer from request metadata, verifies it with the Authenticator, and stashes the verified authz.Principal on the context via middleware.WithPrincipal — plus the raw bearer via middleware.WithInboundBearer, so a delegation TokenSource can act on behalf of the caller when calling another service (WS-028). The authz interceptor then reads the principal through VerifiedPrincipal (wired as its grpcauthz.PrincipalFunc). It is fail-closed: an invalid bearer is rejected with codes.Unauthenticated and the handler never runs.
A nil Authenticator returns a no-op interceptor (identity unchanged) so the stage is inert until a backend is configured.
func VerifiedPrincipal ¶
VerifiedPrincipal is the grpcauthz.PrincipalFunc adapter: it returns the principal the authentication interceptor stashed on ctx, or the zero principal when none is present (an unauthenticated/public path — authz default-denies any non-public method for it). Wire it with grpcauthz.WithPrincipalFunc(authn.VerifiedPrincipal) so the authorizer sees only VERIFIED identities, replacing the unverified DevPrincipalFunc.
Types ¶
type AudienceResolver ¶ added in v0.56.0
type AudienceResolver interface {
// AudienceFor returns the audience configured for target and true, or "" and
// false when target is unmapped (the caller must fail closed on false).
AudienceFor(target string) (audience string, ok bool)
}
AudienceResolver maps a logical outbound target (service name / host) to the audience its tokens must carry. Sourced from config now; apx-catalog-backed later — the resolver seam is what lets the catalog become the ergonomic path without changing call sites.
type Authenticator ¶
type Authenticator interface {
Authenticate(ctx context.Context, bearer string) (authz.Principal, error)
}
Authenticator verifies a bearer and returns the caller's authz.Principal — the Role 3 verify seam. The default backend (authn/oidc) verifies the app bearer against the app's issuer/JWKS (signature + iss/aud/exp) and maps the verified claims to a principal. It MUST fail closed: an invalid, expired, or wrong-issuer token returns an error, never a partial principal.
type AuthenticatorFunc ¶
AuthenticatorFunc adapts a function to an Authenticator.
func (AuthenticatorFunc) Authenticate ¶
func (f AuthenticatorFunc) Authenticate(ctx context.Context, bearer string) (authz.Principal, error)
Authenticate implements Authenticator.
type ClaimsMapper ¶
type ClaimsMapper interface {
// MapClaims returns the authored principal for id within this app, or an error
// (e.g. [ErrNotEntitled]) when the identity may not enter the app.
MapClaims(ctx context.Context, id Identity) (authz.Principal, error)
}
ClaimsMapper authors the app-specific authz.Principal for an Identity — the Role 2 claim-authoring seam (WS-026 §2.1). It confirms the identity is entitled to THIS app (via Identity.Apps) and enriches the principal with the roles/tenant/scopes that drive authz. The dev default is StaticClaimsMapper (a manipulable, hot-reloadable static mapping); production binds a real source (directory / entitlements service).
type Identity ¶
type Identity struct {
// Subject is the stable user identifier (the id_token `sub`).
Subject string
// Name is the human-readable display name, if asserted.
Name string
// Email is the caller's email, if asserted.
Email string
// Apps is the app-access entitlement: the app/client names this identity may
// enter (the same set that drives the IdP launchpad tiles). A [ClaimsMapper]
// uses it to confirm entitlement to a specific app.
Apps []string
// Raw holds any additional coarse claims from the upstream assertion.
Raw map[string]any
}
Identity is the COARSE upstream identity assertion, as produced by the IdP (Role 1): who the caller is and which apps they may enter. It intentionally carries NO in-app roles/tenant/scopes — those are authored downstream by the app identity's ClaimsMapper (Role 2), which is why the app-identity tier exists (WS-026 §2.1 / D11).
type InterceptorOption ¶
type InterceptorOption func(*interceptorConfig)
InterceptorOption configures the authentication interceptor.
func WithMetadataKey ¶
func WithMetadataKey(key string) InterceptorOption
WithMetadataKey overrides the metadata key the bearer is read from (default "authorization").
func WithRequired ¶
func WithRequired() InterceptorOption
WithRequired makes the interceptor reject a request that carries NO bearer with codes.Unauthenticated. The default is optional: a request with no bearer passes through with no principal stashed, so PUBLIC methods still work and non-public methods fail closed downstream at the (default-deny) authorizer. A malformed or INVALID bearer is always rejected regardless of this option.
type Issuer ¶
type Issuer interface {
// Mint returns a signed app bearer encoding p's identity and authored claims.
Mint(ctx context.Context, p authz.Principal) (bearer string, err error)
}
Issuer mints + signs the app bearer for an authored principal — the Role 2 mint seam. iss is the app; aud is the app's microservices. The concrete JOSE-backed implementation lives in authn/oidc (dependency-light root).
type StaticAudiences ¶ added in v0.56.0
StaticAudiences is a map-backed AudienceResolver for config-driven wiring: target (a gRPC service name like "orders.v1.Orders", or a host) -> audience.
func (StaticAudiences) AudienceFor ¶ added in v0.56.0
func (s StaticAudiences) AudienceFor(target string) (string, bool)
AudienceFor implements AudienceResolver.
type StaticClaimsMapper ¶
type StaticClaimsMapper struct {
// AppName is the app/client name entitlement is checked against.
AppName string
// contains filtered or unexported fields
}
StaticClaimsMapper is the dev-default ClaimsMapper: a manipulable, hot-reloadable static mapping from identity Subject to the authz.Principal that identity gets in THIS app. It is easy to edit at runtime (Set / Replace) so a developer can "give bob group X in tenant-b for this app" in seconds without a rebuild — the WS-026 dev-manipulability requirement (D10). It is NOT a production claims source.
Entitlement: when RequireEntitlement is set (via WithRequireEntitlement), MapClaims first confirms the identity's app-access (Identity.Apps) includes AppName and returns ErrNotEntitled otherwise — enforcing the IdP's coarse app-access before authoring any app claims.
func NewStaticClaimsMapper ¶
func NewStaticClaimsMapper(appName string, bySubject map[string]authz.Principal, opts ...StaticClaimsOption) *StaticClaimsMapper
NewStaticClaimsMapper returns a mapper for appName seeded with bySubject (subject -> authored principal for this app). The seed map is copied.
func (*StaticClaimsMapper) MapClaims ¶
MapClaims implements ClaimsMapper. It confirms entitlement (when required), then returns the authored principal for id.Subject with Subject forced to the verified identity. The returned Groups/Scopes/Claims slices/maps are copies so callers cannot mutate the stored mapping.
type StaticClaimsOption ¶
type StaticClaimsOption func(*StaticClaimsMapper)
StaticClaimsOption configures a StaticClaimsMapper.
func WithRequireClaims ¶
func WithRequireClaims() StaticClaimsOption
WithRequireClaims makes MapClaims fail with ErrNoClaims when an entitled identity has no mapping entry (instead of returning a bare Subject-only principal).
func WithRequireEntitlement ¶
func WithRequireEntitlement() StaticClaimsOption
WithRequireEntitlement makes MapClaims fail closed with ErrNotEntitled when the identity's app-access set does not include AppName.
type TokenSource ¶ added in v0.56.0
TokenSource yields the bearer to present when calling another service. TokenFor returns the inbound token unchanged when targetAudience is already one of the caller's audiences (passthrough within a trust domain), or a token scoped to targetAudience obtained via RFC 8693 token exchange otherwise. It fails closed: if it cannot produce a token scoped to targetAudience it returns an error, never the raw inbound token destined for a different audience.
type TokenTopology ¶
type TokenTopology string
TokenTopology selects the token minting/verification topology.
const ( // TwoTier is the default: the app identity mints the app bearer (Role 2) and // microservices verify against the APP's issuer/JWKS (Role 3). Swapping the // upstream IdP is a config change at Role 2 only — no microservice change. TwoTier TokenTopology = "two-tier" // SingleIssuer is the alternative: the IdP mints the audience-scoped bearer // directly and microservices verify against the IDP's issuer/JWKS. Role 2 does // not mint. Falls out of Role 1 + Role 3 because verification is // topology-agnostic. SingleIssuer TokenTopology = "single-issuer" )