azure_ad

package
v0.19.0 Latest Latest
Warning

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

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

Documentation

Overview

Package azure_ad authenticates Microsoft Entra ID (Azure AD) tokens. Composes the Phase 1 oidc.Provider (decision §9.2) for the heavy lifting — signature verify and base claim validation — and layers AAD-specific concerns on top:

  • Tenant lock-in via the `tid` claim
  • Optional Microsoft Graph group enrichment when the JWT's groups claim overflows (AAD truncates at ~200 groups)
  • Correct issuer template for single- vs. multi-tenant

Decision §9.5: standard Bearer flow; no widened-header use.

Audit reason codes (Phase 1 contract):

rejected             — bad signature, expired, tid mismatch,
                       aud mismatch, Graph 401/403
invalid              — missing tid, malformed claims,
                       unsupported alg
provider_unavailable — AAD JWKS down, Graph 5xx
not_for_me           — empty Bearer (delegates to OIDC's looksLikeJWT)

Index

Constants

View Source
const ProviderName = "azure_ad"

ProviderName is the registry name.

Variables

This section is empty.

Functions

func ExtractTenantID

func ExtractTenantID(claims map[string]any) string

ExtractTenantID returns the "tid" claim, or "" if it's missing / non-string. The empty-return form lets callers distinguish "missing" from "wrong tenant" without a typed error.

Types

type Config

type Config struct {
	// TenantID is the Entra tenant GUID. REQUIRED unless AllowMultiTenant
	// is true.
	TenantID string `yaml:"tenant_id"`

	// Audience is REQUIRED. Typically the Application ID URI from the
	// app registration (e.g. "api://forge").
	Audience string `yaml:"audience"`

	// AllowMultiTenant enables accepting tokens from Entra tenants other
	// than the one in TenantID. Defaults to false (single-tenant — safe
	// choice). When true:
	//   - the composed oidc.Provider's issuer-equality check is
	//     suppressed (the "common" issuer template has a {tenantid}
	//     placeholder that string-equality can't satisfy)
	//   - tenancy enforcement moves to AllowedTenants (below); see
	//     CHANGELOG for the security implications
	AllowMultiTenant bool `yaml:"allow_multi_tenant,omitempty"`

	// AllowedTenants is an optional allowlist of Entra tenant GUIDs,
	// matched against the JWT's `tid` claim. Only meaningful when
	// AllowMultiTenant=true; ignored in single-tenant mode (TenantID
	// is the gate there).
	//
	// Empty list + AllowMultiTenant=true = "any tenant globally" —
	// the documented but high-risk shape. Set this list for the safer
	// "these specific tenants only" semantic.
	//
	// Effort to set: customers know their partner tenants; operators
	// just copy GUIDs in. There is no API to enumerate them.
	AllowedTenants []string `yaml:"allowed_tenants,omitempty"`

	// GroupsMode is "claim" (default — uses the in-JWT groups/roles
	// claim) or "graph" (queries Microsoft Graph when groups are missing,
	// i.e. AAD overage).
	GroupsMode string `yaml:"groups_mode,omitempty"`

	// GraphTimeout caps each Graph call. Default 5s. Only used when
	// GroupsMode == "graph".
	GraphTimeout time.Duration `yaml:"graph_timeout,omitempty"`

	// JWKSCacheTTL bounds the JWKS cache age. Defaults to 1h.
	JWKSCacheTTL time.Duration `yaml:"jwks_cache_ttl,omitempty"`

	// GraphEndpoint is a TEST-ONLY override pointing at a fake Graph
	// server. Empty in production.
	GraphEndpoint string `yaml:"-"`
}

Config controls the azure_ad provider.

func (Config) Validate

func (c Config) Validate() error

Validate returns ErrProviderNotConfigured when required fields are missing.

type GraphCache

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

GraphCache holds enriched group memberships keyed by user ID, with a short TTL. Bounds how long a stale "removed from group" state stays cached after AAD's reality changes.

func NewGraphCache

func NewGraphCache(ttl time.Duration) *GraphCache

NewGraphCache builds an empty cache.

func (*GraphCache) Get

func (c *GraphCache) Get(userID string) ([]string, bool)

Get returns the cached groups for userID, or (nil, false) on miss/expiry.

The returned slice is a defensive copy — callers that subsequently mutate their Identity.Groups (the auth.Identity layer treats Groups as a freely- mutable field) MUST NOT corrupt the cache. (Review NIT.)

func (*GraphCache) Put

func (c *GraphCache) Put(userID string, groups []string)

Put stores the groups under userID with a fresh TTL. Overwrites any prior entry (does not extend).

Stores a defensive copy so subsequent caller mutations of the input slice don't reach back through cache hits.

type GraphClient

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

GraphClient calls Microsoft Graph /me/transitiveMemberOf to enrich group memberships when the JWT's groups claim overflows (AAD truncates groups when the user is in more than ~200 of them).

Forge holds NO Graph credentials of its own — the caller's Bearer token is reflected to Graph, which authorizes the read against the user's delegated permission (GroupMember.Read.All).

func NewGraphClient

func NewGraphClient(timeout time.Duration) *GraphClient

NewGraphClient builds a client pointed at the real Graph endpoint.

func NewGraphClientWithEndpoint

func NewGraphClientWithEndpoint(endpoint string, timeout time.Duration) *GraphClient

NewGraphClientWithEndpoint is a TEST-ONLY constructor for pointing at a fake Graph server.

func (*GraphClient) TransitiveMemberOf

func (c *GraphClient) TransitiveMemberOf(ctx context.Context, _ string, authHeader string) ([]string, error)

TransitiveMemberOf walks the paginated response and returns the full list of (transitive) group object IDs the caller belongs to. The authHeader is reflected verbatim — Forge does not authenticate to Graph independently.

Error classification:

401 / 403   → auth.ErrTokenRejected (caller's token missing
              GroupMember.Read.All consent)
5xx / network → auth.ErrProviderUnavailable
@odata.nextLink pointing at a foreign host → error (never followed)

type Provider

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

Provider implements auth.Provider for AAD callers.

func New

func New(cfg Config) (*Provider, error)

New constructs a Provider after validating cfg.

func (*Provider) Name

func (p *Provider) Name() string

Name implements auth.Provider.

func (*Provider) Verify

func (p *Provider) Verify(ctx context.Context, token string, headers auth.Headers) (*auth.Identity, error)

Verify implements auth.Provider.

Jump to

Keyboard shortcuts

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