Documentation
¶
Overview ¶
Package auth authenticates requests (SPEC §8): browser sessions via the built-in OIDC login (Okta, Entra ID, Google…), identity-aware proxies (Google IAP, AWS ALB OIDC, trusted headers), Backyard API tokens, static tokens and IdP-issued bearer JWTs (device flow). In mode "none" everyone is an anonymous guest identified by a cookie.
Index ¶
- Constants
- Variables
- func MediaType(contentType string) string
- func WithClientIP(ctx context.Context, ip string) context.Context
- type Authenticator
- type Grant
- type MCP
- func (m *MCP) AllowedOrigin(origin string) bool
- func (m *MCP) Challenge(err error) string
- func (m *MCP) Close(ctx context.Context) error
- func (m *MCP) GrantByID(ctx context.Context, id string) (*Grant, error)
- func (m *MCP) Grants(ctx context.Context, userID string) ([]Grant, error)
- func (m *MCP) Issuer() string
- func (m *MCP) MaxBodyBytes() int64
- func (m *MCP) Resource() string
- func (m *MCP) ResourceMetadataURL() string
- func (m *MCP) Revoke(ctx context.Context, id string, actor *identity.User, via string) (bool, error)
- func (m *MCP) RevokeUser(ctx context.Context, userID string, actor *identity.User, via string) (int, error)
- func (m *MCP) ServeMetadata(w http.ResponseWriter, r *http.Request)
- func (m *MCP) ServeOAuth(w http.ResponseWriter, r *http.Request)
- func (m *MCP) VerifyAccessToken(ctx context.Context, raw string) (*identity.User, *Grant, error)
- type Options
- type TokenClaims
- type Tokens
Constants ¶
const ( ModeNone = "none" ModeOIDC = "oidc" ModeIAP = "iap" ModeALB = "alb" ModeHeader = "header" )
Normalized auth modes.
const ( PathPrefix = "/__backyard/auth/" LoginPath = PathPrefix + "login" CallbackPath = PathPrefix + "callback" LogoutPath = PathPrefix + "logout" CLIPath = PathPrefix + "cli" )
Paths served by Handler.
const ( SessionCookie = "backyard_session" // OAuthCookie holds the login state when server.public_url is http. OAuthCookie = "backyard_oauth" // OAuthHostCookie holds the login state when server.public_url is https: // browsers accept a "__Host-" cookie only from the host itself (Secure, // Path=/, no Domain), so a site's page cannot plant one on the hub. OAuthHostCookie = "__Host-" + OAuthCookie AnonCookie = "backyard_anon" )
Cookie names.
const ( // IAPHeader carries the signed IAP assertion. IAPHeader = "X-Goog-IAP-JWT-Assertion" // IAPIssuer is the "iss" of IAP assertions. IAPIssuer = "https://cloud.google.com/iap" // DefaultIAPJWKSURL publishes IAP's ES256 signing keys. DefaultIAPJWKSURL = "https://www.gstatic.com/iap/verify/public_key-jwk" )
const ( MCPPath = "/__backyard/mcp" OAuthPathPrefix = "/__backyard/oauth/" OAuthRegisterPath = OAuthPathPrefix + "register" OAuthTokenPath = OAuthPathPrefix + "token" OAuthRevokePath = OAuthPathPrefix + "revoke" ASMetadataPath = "/.well-known/oauth-authorization-server" ResourceMetadataPath = "/.well-known/oauth-protected-resource" + MCPPath )
Paths of the connector (SPEC §8.4, §14.1). Only AuthorizePath stays behind an identity-aware proxy; the others authenticate by themselves.
const ( // TokenIssuer is the "iss" claim of Backyard API tokens. TokenIssuer = "backyard" // TokenScope is the "scope" claim of Backyard API tokens. TokenScope = "api" // DefaultTokenTTL is used when NewTokens is given a non-positive TTL. DefaultTokenTTL = 720 * time.Hour )
const (
// ALBHeader carries the ALB-signed user claims.
ALBHeader = "X-Amzn-Oidc-Data"
)
const AuthorizePath = "/__backyard/auth/authorize"
AuthorizePath is served by Authenticator.Handler() (GET, POST).
Variables ¶
var ( // ErrUnauthenticated: no (valid) credentials were presented. ErrUnauthenticated = errors.New("auth: unauthenticated") // ErrForbidden: authenticated but not allowed (domain/group gate). ErrForbidden = errors.New("auth: forbidden") // ErrCookieConflict: the browser sent valid session (or guest) cookies // of different users, e.g. one planted by a site's page. It is always // wrapped together with ErrUnauthenticated. ErrCookieConflict = errors.New("auth: the browser sent sign-in cookies of different users") )
var ErrInvalidToken = errors.New("auth: invalid MCP access token")
ErrInvalidToken marks an MCP access token that does not authenticate: answer 401 with MCP.Challenge(err).
Functions ¶
Types ¶
type Authenticator ¶
type Authenticator interface {
Authenticate(w http.ResponseWriter, r *http.Request) (*identity.User, error)
// LoginURL returns the URL a browser should be redirected to in order to
// log in and come back to returnTo, or "" when interactive login is not
// handled by Backyard (iap/alb/header/none).
LoginURL(returnTo string) string
// Handler serves /__backyard/auth/* on the hub host (login, callback,
// logout, cli). It expects the full request path.
Handler() http.Handler
// Mode returns the normalized auth mode.
Mode() string
}
Authenticator resolves the user behind a request.
Authenticate returns (user, nil) on success, (nil, ErrUnauthenticated) when the request carries no usable credentials, (nil, ErrForbidden) when the user is authenticated but rejected by allowed_domains/allowed_emails/allowed_groups, or another error for infrastructure failures. Bearer tokens (API, static, IdP JWT) are accepted in every mode. In mode "none" Authenticate only fails when an invalid bearer token is presented, or when the browser sends valid guest cookies of different guests (ErrUnauthenticated, with ErrCookieConflict); otherwise it returns an anonymous user and, when w is non-nil, sets the backyard_anon cookie. On a cookie conflict Authenticate expires the cookies Backyard did not set itself when w is non-nil.
func New ¶
New builds the authenticator for cfg.Auth.Mode. For mode "oidc" it performs OIDC discovery against the issuer (ctx bounds that call).
HTTPClient defaults to a client with a 15 s timeout (a client without a timeout gets one). When auth.oidc.issuer is configured in a mode other than oidc (device flow behind a proxy), discovery for IdP bearer tokens is attempted here and, if the IdP is unreachable, retried lazily.
func NewBearer ¶
NewBearer builds a bearer-only authenticator for the standalone deploy gateway: it accepts Backyard API tokens, static tokens and (if cfg.Auth.OIDC.Issuer is set) IdP-issued JWTs — never cookies or proxy headers. Handler() serves nothing and LoginURL returns "".
IdP discovery is attempted here but never fails construction: when the IdP is unreachable it is retried lazily when the first IdP token arrives.
type Grant ¶ added in v0.2.0
type Grant struct {
ID, ClientID, ClientName, ClientKind, ClientHost, RedirectURI, Scope, Resource string
// User is the snapshot taken at consent: ID, Email, Name, Groups, Anonymous.
User identity.User
CreatedAt, ExpiresAt time.Time
}
Grant is one connection: the consent of a user to one MCP client.
type MCP ¶ added in v0.2.0
type MCP struct {
// contains filtered or unexported fields
}
MCP is the connector's authorization server. It is nil when mcp.enabled is false.
func MCPOf ¶ added in v0.2.0
func MCPOf(a Authenticator) *MCP
MCPOf returns the authorization server of a (nil when MCP is disabled, a is bearer-only, or a is not built by New).
func (*MCP) AllowedOrigin ¶ added in v0.2.0
AllowedOrigin reports whether a present Origin header is accepted at /__backyard/mcp besides the hub origin, which the caller checks (SPEC §14.1, gate step 3): the origins of the https entries of mcp.redirect_uris and of the entries of mcp.clients (P is accepted too).
func (*MCP) Challenge ¶ added in v0.2.0
Challenge returns the exact WWW-Authenticate value for err (SPEC §14.1); a nil err gives the missing-token form.
func (*MCP) Close ¶ added in v0.2.0
Close waits for the background work of the authorization server (grant pruning) to finish, or for ctx to end. Call it after the HTTP server has stopped and before the document store is closed.
func (*MCP) GrantByID ¶ added in v0.2.0
GrantByID returns a grant, or nil, nil when it does not exist.
func (*MCP) Grants ¶ added in v0.2.0
Grants lists the live grants of userID, newest first, at most 200.
func (*MCP) MaxBodyBytes ¶ added in v0.2.0
MaxBodyBytes is the MCP request body limit: 2 × mcp.max_publish_bytes + 1 MiB.
func (*MCP) ResourceMetadataURL ¶ added in v0.2.0
ResourceMetadataURL returns M, the protected resource metadata URL.
func (*MCP) Revoke ¶ added in v0.2.0
func (m *MCP) Revoke(ctx context.Context, id string, actor *identity.User, via string) (bool, error)
Revoke deletes one grant; via names the lever (rfc7009, hub, policy, replay).
func (*MCP) RevokeUser ¶ added in v0.2.0
func (m *MCP) RevokeUser(ctx context.Context, userID string, actor *identity.User, via string) (int, error)
RevokeUser deletes every grant of userID and returns how many there were.
func (*MCP) ServeMetadata ¶ added in v0.2.0
func (m *MCP) ServeMetadata(w http.ResponseWriter, r *http.Request)
ServeMetadata serves both discovery documents (SPEC §8.4.2).
func (*MCP) ServeOAuth ¶ added in v0.2.0
func (m *MCP) ServeOAuth(w http.ResponseWriter, r *http.Request)
ServeOAuth serves /__backyard/oauth/{register,token,revoke}.
func (*MCP) VerifyAccessToken ¶ added in v0.2.0
VerifyAccessToken returns the re-gated user and the grant of an MCP access token. errors.Is(err, ErrInvalidToken) means 401 invalid_token (Challenge gives the header); any other error is an infrastructure failure (503).
type Options ¶
type Options struct {
// Directory enriches users after claim mapping (optional).
Directory identity.Directory
// HTTPClient is used for OIDC discovery and JWKS/key fetches (default: a
// client with a 15 s timeout; a client without a timeout gets one).
HTTPClient *http.Client
// Logger (default slog.Default()).
Logger *slog.Logger
// Now overrides the clock (tests).
Now func() time.Time
// Docs stores the grants of the MCP connector's authorization server;
// required by New when mcp.enabled is true (nil for NewBearer).
Docs docstore.Store
// MCPHTTPClient replaces the SSRF-guarded client that fetches client
// metadata documents (tests only). Redirects are never followed and the
// 5 s timeout applies either way.
MCPHTTPClient *http.Client
// MCPAllow charges one request of the general API limit
// (ratelimit.api_per_minute) for key at the connector's authorize and
// token endpoints and reports how long to wait when it is spent (nil:
// no limit). The server passes its AllowAPI.
MCPAllow func(key string) (bool, time.Duration)
}
Options are optional dependencies for authenticators.
type TokenClaims ¶
type TokenClaims struct {
Subject string `json:"sub"`
Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"`
Groups []string `json:"groups,omitempty"`
Admin bool `json:"admin,omitempty"`
Scope string `json:"scope"`
}
TokenClaims are the claims of a Backyard API token (SPEC §8.3).
type Tokens ¶
type Tokens struct {
// contains filtered or unexported fields
}
Tokens mints and verifies Backyard API tokens (SPEC §8.3).
Tokens are HS256 JWTs keyed with keys.Derive(server.secret, keys.APIToken) carrying {iss:"backyard", sub, email, name, groups, admin, scope:"api", jti, iat, exp}. Rotating server.secret revokes every token. A Tokens value is immutable and safe for concurrent use.
func (*Tokens) Issue ¶
func (t *Tokens) Issue(u *identity.User, ttl time.Duration) (token string, expiresAt time.Time, err error)
Issue mints a token for u with scope "api". ttl <= 0 uses the default TTL.
The token always fits the size Verify accepts: an overlong name is shortened and, if needed, trailing groups are dropped (callers that care which groups survive put the important ones first).
func (*Tokens) Verify ¶
Verify validates a token and returns the user it represents.
Only HS256 is accepted (no "none", no asymmetric algorithms); iss must be "backyard", scope "api", and exp/iat are checked with 30 seconds of leeway. Errors wrap ErrUnauthenticated. The returned user carries Admin as recorded in the token; subjects starting with "anon:" are anonymous.