authn
import "github.com/infrapi/kayra/internal/authn"
Validates an incoming bearer JWT, resolves the user id it identifies, and
— for a claims-backed authorizations.<name> entry only — the groups
and attributes too, straight out of that same already-verified token:
the steps between “a request carries an Authorization: Bearer ...
header” and “this is jdoe, authenticated via okta, member of
sandbox-core-sysadmin.”
Verify#
v, err := authn.Verify(manager, tokenString)
type Verified struct {
Provider string
Claims jwt.MapClaims
Token string
}
Parses the token, reads its kid header, and locates the owning provider
and public key via oidc.Manager.FindKey — so a bad or
unknown kid fails before any signature check runs. Only RS256/RS384/
RS512 are accepted. Beyond signature validity, two checks the JWT library
doesn’t do on its own: iat must not be in the future, exp (if present)
must not be in the past — the spec’s “obviously then token must contain
‘iat’ and can’t be in future, and ’exp’ can’t be expired.”
ttl := v.ExpiresIn(fallback)
Returns the duration until exp, or fallback when the token carries none
or it’s already non-positive — this is the TTL
authz.Cached.ResolveUser caches the resolved user
under, per the spec’s “cache … for ’exp’ duration.”
ResolveUserID#
userID, err := authn.ResolveUserID(v, cfg, remoteURL, proxy)
cfg is a provider’s remote.user_id block
(config.UserIDConfig); remoteURL/proxy
are that same provider’s remote.url/remote.proxy:
from: "token"— readscfg.Fielddirectly out ofv.Claims;remoteURL/proxygo unused.from: "userinfo_endpoint"— fetches the provider’s discovery document to finduserinfo_endpoint(throughproxy, if set — see oidc.ClientFor), calls it with the bearer token, and readscfg.Fieldout of the JSON response.
Either way, cfg.Filter (if set) is a regexp applied to the raw value,
first capture group wins — e.g. ^([^@]+)@ to turn jdoe@company.com
into jdoe.
ResolveGroups#
groups, err := authn.ResolveGroups(v, from, field, filter, remoteURL, proxy)
from/field/remoteURL/proxy are the exact same sources
ResolveUserID reads from (a token claim, or the provider’s userinfo
response) — just applied to a list claim instead of a scalar one, for
an authorizations.<name>.claims entry’s from/groups_field
(config.ClaimsAuthzConfig).
field must hold a JSON array of strings (e.g. Okta’s own MemberOf).
filter, when set, is applied independently to each element exactly
like authorizations.<name>.ldap.user.groups.member_regex does to a
real memberOf value: an element that doesn’t match is dropped, one
that does contributes its first capture group (or the whole match,
when filter has no capture group) to the result. When
from: "userinfo_endpoint", v caches the fetched response on first
use (Verified.fetchUserinfo), so calling this after ResolveUserID
(or ResolveAttributes) for the same token costs no extra network
call — the identical response already fetched is just read again for
this field.
ResolveAttributes#
attrs, err := authn.ResolveAttributes(v, from, userAttributes, remoteURL, proxy)
ResolveGroups’ sibling for the attributes half of a claims entry:
userAttributes is the same local-name -> field mapping
authorizations.<name>.ldap.user.user_attributes uses
(config.ClaimsAuthzConfig.UserAttributes), read from the same
from/remoteURL/proxy source. A named field absent from the source, or
not a string, resolves to "" rather than failing the whole
resolution or omitting the key — exactly how a missing LDAP attribute
already behaves, so a caller never has to treat the two backends
differently.