appauth

package
v0.2.17 Latest Latest
Warning

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

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

Documentation

Overview

Package appauth is the hub's login-time authorization endpoint for published applications.

The design deliberately keeps the hub OFF the per-request data path:

  • Public apps never contact the hub at all.
  • Private apps redirect an unauthenticated browser here exactly once. The hub reuses the shared portal browser session (or bounces through the normal /login flow), evaluates ONE SubjectAccessReview against the tenant workspace, and hands the app's access proxy a one-use code. The proxy exchanges the code server-to-server and then maintains its own bounded local session; the hub is not consulted again until that session expires.
  • Non-browser clients (CLIs, CI, agents) exchange their hub bearer AT THE HUB for a short-lived app access token bound to one instance (POST /auth/apps/token) and present that to the app. The proxy asks the hub once (POST /auth/apps/verify), then caches the verdict locally for at most the session TTL. A gate never sees a hub bearer (see apptoken.go and verify.go).

Access policy is plain kcp RBAC in the tenant workspace: visiting a private app requires `get` on the instance resource with subresource `access` (e.g. `applications/my-shop` + subresource `access`). Granting a member access is creating a ClusterRole/ClusterRoleBinding pair; revoking is deleting it. The hub carries no knowledge of any provider CRD schema — only resource coordinates supplied by the (controller-authored) proxy config, validated syntactically here.

Index

Constants

View Source
const (
	// AuthorizePath is the browser-facing entry point the access proxy
	// redirects to when a private app has no session.
	AuthorizePath = "/auth/apps/authorize"
	// ExchangePath is the server-to-server endpoint the access proxy calls to
	// turn a one-use code into a session grant.
	ExchangePath = "/auth/apps/exchange"
	// TokenPath is where a caller exchanges their hub bearer for an app
	// access token bound to one instance (see HandleMintToken).
	TokenPath = "/auth/apps/token"
	// VerifyPath is the server-to-server endpoint the access proxy calls to
	// authorize a request carrying an app access token (see HandleVerify).
	VerifyPath = "/auth/apps/verify"
	// CallbackPath is the reserved path on the app host that authorize
	// redirects back to. It must stay in lockstep with the access proxy's
	// callback route (providers/infrastructure/accessproxy).
	CallbackPath = "/__railgrid/auth/callback"

	// AccessSubresource is the RBAC convention gating private apps: a visitor
	// needs `get` on `<resource>/<name>` with this subresource in the tenant
	// workspace. The subresource is never served; it exists only as an
	// authorization tuple.
	AccessSubresource = "access"
	// AccessVerb is the verb checked against the access subresource.
	AccessVerb = "get"
)
View Source
const (
	// AppTokenPrefix marks an app access token. The access proxy mirrors it
	// (providers/infrastructure/accessproxy) and refuses to relay anything
	// else, so a raw hub token presented at a gate never leaves that gate.
	AppTokenPrefix = "fapp_"
)
View Source
const PreviewHandoffPath = "/auth/apps/preview-handoff"

PreviewHandoffPath exchanges a scoped workload credential for one-app browser access.

Variables

View Source
var ErrInstanceNotPublished = errors.New("instance has no published host")

ErrInstanceNotPublished is returned by an InstanceHostResolver when the referenced instance has no published host to pin the redirect against.

View Source
var ErrInvalidPreviewIdentity = errors.New("invalid preview identity")

ErrInvalidPreviewIdentity marks a caller credential that was absent, malformed, unauthenticated, or resolved to an identity outside the requested workspace service-account contract. Other resolver errors represent an identity-verification dependency failure and must not be charged as a bad credential.

Functions

func NewKCPPreviewIdentityResolver added in v0.2.14

func NewKCPPreviewIdentityResolver(config *rest.Config) func(*http.Request, InstanceRef) (browsersession.Identity, error)

NewKCPPreviewIdentityResolver authenticates in the requested workspace. A provider account from another cluster must never become a tenant-local SA merely because both accounts have the same username.

Types

type CodeRecord

type CodeRecord struct {
	Purpose      string
	Ref          InstanceRef
	RedirectHost string
	Identity     browsersession.Identity
	ExpiresAt    time.Time
}

CodeRecord is what authorize binds a code to and exchange verifies against. It carries identity metadata only — never a credential.

type CodeStore

type CodeStore interface {
	Put(ctx context.Context, code string, record CodeRecord) error
	Take(ctx context.Context, code string) (CodeRecord, bool)
}

CodeStore persists one-use authorization codes between the browser's authorize hop and the access proxy's server-to-server exchange.

Take MUST be atomic across every hub replica: a code that two callers can redeem is a replayable app sign-in.

type Config

type Config struct {
	// PreviewIdentity verifies a workspace service account for a scoped preview.
	// It should return ErrInvalidPreviewIdentity only for caller credential or
	// identity rejection; other errors are treated as transient dependencies.
	PreviewIdentity func(*http.Request, InstanceRef) (browsersession.Identity, error)
	// Sessions is the shared hub browser-session store (portal SSO).
	Sessions *browsersession.Store
	// PreviewSessions stores short-lived one-app browser sessions separately
	// from portal sessions. A separate backend is required for mixed-version
	// hubs because older replicas ignore unknown scoped identity fields.
	PreviewSessions *browsersession.Store
	// SARClient resolves per-workspace SubjectAccessReview clients.
	SARClient SARFactory
	// InstanceHost resolves the published host of the instance a sign-in is
	// for. Redirects are only issued to exactly that host.
	InstanceHost InstanceHostResolver
	// Codes persists the one-use authorization codes. It defaults to a
	// bounded process-local map, which is only correct for a single hub
	// replica: authorize and exchange are separate requests and a scaled hub
	// serves them from different pods. Supply a shared store (see
	// pkg/hub/sharedstore) whenever the hub runs more than one replica.
	Codes CodeStore
	// PreviewCodes stores scoped handoff and final app-authorization codes in
	// an isolated collection. Ordinary Codes must never be used as a fallback.
	PreviewCodes CodeStore
	// LoginPath is the hub-relative path of the interactive login page an
	// unauthenticated browser is sent to. Defaults to "/ui/login" (the
	// portal SPA is mounted under /ui/).
	LoginPath string
	// BearerIdentity authenticates the Authorization bearer on a request and
	// resolves it to the platform identity. It MUST be the same validator the
	// hub uses to mint browser sessions from a bearer (KCPProxy.BrowserIdentity),
	// so the token endpoint admits exactly the identities the browser flow
	// admits. Nil leaves the token endpoint unregistered.
	BearerIdentity func(*http.Request) (browsersession.Identity, error)
	// TokenKey returns the hub secret (>= 32 bytes, identical on every
	// replica) that app access tokens are HKDF-derived from. Nil leaves both
	// the token and verify endpoints unregistered.
	TokenKey func(context.Context) ([]byte, error)
	// IdentityUnavailable is the sentinel BearerIdentity wraps when the
	// credential was accepted but the identity could not be resolved (a
	// transient hub-side failure). Such errors answer 503 and are not counted
	// as failed verifications. Nil treats every BearerIdentity error as an
	// invalid credential.
	IdentityUnavailable error
	// ClientIP keys the failed-verification limiter. It should see through
	// the hub's trusted reverse proxies (proxy.ClientIP). Nil uses the
	// connection peer address.
	ClientIP func(*http.Request) string
	// Now and Random are test seams.
	Now    func() time.Time
	Random io.Reader
}

Config assembles a Handler.

type ExchangeResponse

type ExchangeResponse struct {
	Allowed           bool   `json:"allowed"`
	UserID            string `json:"userId"`
	Email             string `json:"email,omitempty"`
	Name              string `json:"name,omitempty"`
	SessionTTLSeconds int64  `json:"sessionTtlSeconds"`
}

ExchangeResponse is returned to the proxy on a successful exchange. It contains identity metadata only — never a credential of any kind.

type Handler

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

Handler serves the authorize, exchange and verify endpoints.

func New

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

New validates cfg and returns a Handler.

func (*Handler) HandleAuthorize

func (h *Handler) HandleAuthorize(w http.ResponseWriter, r *http.Request)

HandleAuthorize is the browser entry point.

GET /auth/apps/authorize?cluster=&group=&resource=&name=&redirect_uri=&state=

func (*Handler) HandleExchange

func (h *Handler) HandleExchange(w http.ResponseWriter, r *http.Request)

HandleExchange consumes a one-use code.

POST /auth/apps/exchange  {"code":..., "host":..., "cluster":..., ...}

func (*Handler) HandleMintToken

func (h *Handler) HandleMintToken(w http.ResponseWriter, r *http.Request)

HandleMintToken exchanges the caller's hub bearer for an app access token.

POST /auth/apps/token
Authorization: Bearer <hub token>
{"cluster":..., "group":..., "resource":..., "name":..., "ttlSeconds":600}

The caller is authenticated with the same validator that mints browser sessions from a bearer (Config.BearerIdentity) and must pass the same SubjectAccessReview as a browser sign-in. Answers 200 MintResponse, 400 malformed, 401 not a hub user credential, 403 no access grant, 404 instance not published, 429 source over its failure budget, 503 dependency outage.

func (*Handler) HandlePreviewHandoff added in v0.2.14

func (h *Handler) HandlePreviewHandoff(w http.ResponseWriter, r *http.Request)

HandlePreviewHandoff issues a short-lived, one-app browser capability. It never creates a portal session or accepts an asserted end-user identity. Existing name-scoped instances/proxy rights authorize the same app here.

func (*Handler) HandleVerify

func (h *Handler) HandleVerify(w http.ResponseWriter, r *http.Request)

HandleVerify authorizes a non-browser request at a private app's gate.

POST /auth/apps/verify
Authorization: Bearer <app access token>
{"cluster":..., "group":..., "resource":..., "name":...}

Only app access tokens minted by HandleMintToken are accepted — never a hub bearer, which a gate must not be trusted with. The token must be bound to exactly the instance the gate names and unexpired, and the SubjectAccessReview is re-run so a revoked grant stops working within the gate's cache window. Answers 200 VerifyResponse, 400 malformed, 401 not a valid token for this instance, 403 no access grant, 429 source over its failure budget, 503 dependency outage.

Like HandleExchange this endpoint does not authenticate the calling gate — gates hold no credentials by design. It is safe to expose because the request is self-authenticating: nothing is disclosed unless the caller holds a valid app token, and then only the token's own user ID and its yes/no.

func (*Handler) RegisterRoutes

func (h *Handler) RegisterRoutes(router *mux.Router, limit func(http.HandlerFunc) http.HandlerFunc)

RegisterRoutes mounts the endpoints. limit wraps each handler with the hub's auth rate limiter; pass nil to mount unwrapped (tests).

type InstanceHostResolver

type InstanceHostResolver func(ctx context.Context, ref InstanceRef) (string, error)

InstanceHostResolver reports the public host a published instance is served on — the FQDN the provider deployment stamped onto the instance itself. Redirects are pinned to exactly this host, which is what lets published apps live under ANY zone (platform, BYO edge, or a customer-owned domain) without the hub keeping a domain registry: the instance is the single source of truth for where its traffic terminates.

ErrInstanceNotPublished means the instance exists but exposes no host (or does not exist); anything else is an infrastructure failure and fails the authorize closed with a retryable error page.

func NewKCPInstanceHostResolver

func NewKCPInstanceHostResolver(kcpConfig *rest.Config) (InstanceHostResolver, error)

NewKCPInstanceHostResolver returns an InstanceHostResolver that reads the referenced instance in its tenant workspace through the hub's admin kcp config and reports the host the provider stamped onto it.

The host is taken from spec.values.expose.fqdn — the field the instance controller stamps on every exposed instance, i.e. on exactly the set of instances whose access gate can ever start this flow — falling back to status.host for resources that mirror it there instead. A missing object and a missing host are deliberately the same ErrInstanceNotPublished: authorize has already SAR-checked the caller against these coordinates, and collapsing the two keeps the error page from confirming which one it was.

The instance's group version is discovered per call (one GET of /apis/<group> before the instance read). Sign-ins are per-user-per-app-session rare and already cost a SubjectAccessReview, so two extra reads beat carrying a discovery cache that can go stale across provider upgrades.

type InstanceRef

type InstanceRef struct {
	Cluster  string // logical cluster ID of the tenant workspace
	Group    string // API group of the instance CRD
	Resource string // REST plural of the instance CRD
	Name     string // instance name
}

InstanceRef are the resource coordinates of a published template instance. They are authorization inputs only; the hub never reads the object.

type MintRequest

type MintRequest struct {
	Cluster    string `json:"cluster"`
	Group      string `json:"group"`
	Resource   string `json:"resource"`
	Name       string `json:"name"`
	TTLSeconds int64  `json:"ttlSeconds,omitempty"`
}

MintRequest is the body of POST /auth/apps/token: the published instance to mint an app access token for, in the same coordinates the access gate uses (they appear in the query of the gate's sign-in redirect and in its 401 body). TTLSeconds is optional: 0 means the default (600), otherwise 60..900.

type MintResponse

type MintResponse struct {
	Token     string    `json:"token"`
	ExpiresAt time.Time `json:"expiresAt"`
	Host      string    `json:"host"`
}

MintResponse is the body of a successful mint. Token is sent to the app as `Authorization: Bearer <token>`; Host is the instance's published host.

type SARFactory

type SARFactory func(clusterID string) (authorizationv1client.SubjectAccessReviewInterface, error)

SARFactory returns a SubjectAccessReview client scoped to one logical cluster. The hub wires this from its kcp admin config; tests use fakes.

type VerifyResponse

type VerifyResponse struct {
	Allowed           bool   `json:"allowed"`
	UserID            string `json:"userId"`
	SessionTTLSeconds int64  `json:"sessionTtlSeconds"`
}

VerifyResponse is returned to the gate on an allowed verify.

Jump to

Keyboard shortcuts

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