Identity, authorization and context reference¶
What a successful verification gives you, and what you can do with it.
Identity¶
The verified outcome of authentication. What each field holds depends on which verifier produced it:
| Field | API key | JWT | mTLS |
|---|---|---|---|
Subject |
the entry's Subject label |
the sub claim |
the derived certificate subject |
Method |
"apikey" |
"jwt" |
"mtls" |
Claims |
nil |
every verified claim | nil |
Scopes |
nil |
parsed from scope or scp |
nil |
Subject can be empty on a successful verification¶
Subject is not guaranteed non-empty. Two cases produce "" with no error:
- an API-key entry configured without a
Subject; - a JWT with no
subclaim —subis not required, and a token missing it verifies successfully with an empty subject.
The mTLS verifier is the exception: it rejects a certificate whose subject derivation
returns "". If your authorization depends on Subject, check it for emptiness rather
than assuming a verified identity is a named one.
Claims holds JSON types, not Go types¶
Claims comes straight from JSON decoding, so every number is a float64, every array
is a []any, and every object is a map[string]any. This matters for
RequireClaim, which compares by deep equality.
Scopes only parses a space-delimited string¶
Scopes is filled from the scope claim if it is a non-empty string, otherwise
from scp if that is a non-empty string, otherwise it is nil. scope is checked
first, so a token carrying both uses scope and ignores scp.
A scope claim that is a JSON array — which some issuers emit — yields no scopes
at all and no error. RequireScopes will then deny every request, and nothing tells
you why. Read Identity.Claims["scope"] yourself in that case.
Verifier¶
Implemented by the API-key and JWT verifiers. The credential is the raw bearer token or
key, already extracted from the transport — authn never parses a header.
CertVerifier¶
type CertVerifier interface {
VerifyCert(ctx context.Context, verifiedChains [][]*x509.Certificate) (*Identity, error)
}
Implemented by the mTLS verifier only. It is a separate interface because a certificate is a property of the connection rather than a string a caller supplies, so there is no credential to pass.
CertVerifier and Verifier are unrelated types. A helper that takes a Verifier will
not accept an mTLS verifier, and there is no adapter between them in the package.
ErrUnauthenticated¶
The sentinel every credential rejection wraps. Test for it with errors.Is:
switch {
case err == nil:
// authenticated
case errors.Is(err, authn.ErrUnauthenticated):
// a rejected credential — expected, log quietly
default:
// an operational failure (network, JWKS unreachable) — log loudly
}
The distinction is for your logs. The client sees the same generic 401 either way.
AuthorizeFunc¶
The entire authorization surface. It runs after successful verification; false maps
to 403 / PermissionDenied. There is no policy engine, no RBAC model and no rule
DSL — the predicate is Go code you write, and it can call out to whatever external
authorization you have.
It runs on every authenticated request, so keep it fast and side-effect free.
authn never calls an AuthorizeFunc itself — your middleware or interceptor does, so
whether a nil Identity can reach it is up to your call site. The built-in
combinators handle nil defensively; a hand-written predicate should too.
RequireScopes¶
Admits an identity only if Identity.Scopes contains all of the named scopes.
- A
nilidentity is denied. - With no scopes named, it admits any non-
nilidentity — including one with no scopes at all.RequireScopes()is therefore "authenticated is enough", not "deny everything". - API-key and mTLS identities have
nilscopes, so any named scope denies them.
RequireClaim¶
Admits an identity only if Identity.Claims[name] deep-equals value
(reflect.DeepEqual, which avoids a panic on a non-comparable claim value).
- A
nilidentity, or one withnilClaims, is denied. API-key and mTLS identities always havenilclaims, soRequireClaimdenies them unconditionally. - Types must match exactly. Claims are decoded from JSON, so a numeric claim is a
float64:RequireClaim("tier", 1)never matches a token carrying"tier": 1, whileRequireClaim("tier", float64(1))does. - Array claims need an array value. A
groupsclaim of["platform-team"]decodes to[]any{"platform-team"}, soRequireClaim("groups", "platform-team")is always false. Either compare against[]any{"platform-team"}— which requires the whole array to match, not just membership — or write a predicate that ranges over the claim.
There is no built-in "claim contains" or "any-of" combinator. Membership tests are a hand-written predicate.
RequestMetadata¶
The minimal transport facts a predicate may want. Method is the HTTP method (GET,
POST, …) or "grpc" for an RPC; Path is the HTTP request path or the gRPC full
method name.
Nothing in authn populates it. Your adapter calls ContextWithRequestMetadata before
running authorization, and a predicate that reads it must handle the metadata being
absent.
Context helpers¶
func ContextWithIdentity(ctx context.Context, id *Identity) context.Context
func IdentityFromContext(ctx context.Context) (*Identity, bool)
func ContextWithRequestMetadata(ctx context.Context, m RequestMetadata) context.Context
func RequestMetadataFromContext(ctx context.Context) (RequestMetadata, bool)
Both pairs use unexported context keys, so nothing outside the package can collide with them or overwrite a stored value by accident. The HTTP and gRPC adapters share the same keys, which is what lets a handler read the identity the same way regardless of transport.
The bool is false when nothing was stored. IdentityFromContext also returns
false if a different type was stored under the key, which cannot happen through these
functions.
A stored nil identity reports ok == true¶
ContextWithIdentity(ctx, nil) stores a typed nil, and IdentityFromContext then
returns (nil, true) — the ok flag says "something was stored", not "something
usable was stored". A handler written as if id, ok := IdentityFromContext(ctx); ok {
use(id.Subject) } panics on that context. Only store an identity you actually got back
from a verifier, and check for nil if the value could have come from elsewhere.