authn

package
v0.64.0 Latest Latest
Warning

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

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

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

View Source
const DefaultMetadataKey = "authorization"

DefaultMetadataKey is the incoming metadata key the bearer is read from.

Variables

View Source
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.

View Source
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

func VerifiedPrincipal(ctx context.Context) (authz.Principal, error)

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

type AuthenticatorFunc func(ctx context.Context, bearer string) (authz.Principal, error)

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

type StaticAudiences map[string]string

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.

func (*StaticClaimsMapper) Replace

func (m *StaticClaimsMapper) Replace(bySubject map[string]authz.Principal)

Replace swaps the entire subject->principal mapping atomically (hot-reload of a whole claims file). The provided map is copied.

func (*StaticClaimsMapper) Set

func (m *StaticClaimsMapper) Set(subject string, p authz.Principal)

Set installs (or replaces) the authored principal for one subject. Safe for concurrent use — this is the hot-reload path for editing a single identity's app claims live.

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

type TokenSource interface {
	TokenFor(ctx context.Context, targetAudience string) (string, error)
}

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"
)

Directories

Path Synopsis
oidc module

Jump to

Keyboard shortcuts

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