Documentation
¶
Overview ¶
Package scheduledauth provides authentication for the /api/scheduled/* endpoints invoked by Cloud Scheduler / Logic Apps. Two modes are supported:
oidc: Google-issued OIDC ID tokens (RS256, JWKS-validated). Used by GCP Cloud Run and GKE deployments where Cloud Scheduler signs an OIDC token with a service-account audience. Subject pinning to the scheduler service account email(s) is REQUIRED — the audience alone is not sufficient if other tenants in the same GCP org could mint tokens with the same `aud` value.
bearer: shared-secret HMAC-style bearer (constant-time compare). Used by Azure Container Apps + Logic Apps where the workflow pulls the secret from Key Vault at invocation time.
Design context: GitHub PR #161 / issue #159.
Index ¶
Constants ¶
const ( EnvAuthMode = "SCHEDULED_TASK_AUTH_MODE" // "oidc" | "bearer" | "disabled" EnvOIDCIssuer = "SCHEDULED_TASK_OIDC_ISSUER" // default GoogleIssuer EnvOIDCJWKSURL = "SCHEDULED_TASK_OIDC_JWKS_URL" // default GoogleJWKSURL EnvOIDCAudience = "SCHEDULED_TASK_OIDC_AUDIENCE" // comma-separated EnvOIDCSubjects = "SCHEDULED_TASK_OIDC_SUBJECTS" // comma-separated; REQUIRED in oidc mode EnvBearerSecret = "SCHEDULED_TASK_SECRET" // bearer mode only )
Environment variable names. Centralized so tests and Terraform stay aligned with the Go code.
const DefaultClockSkew = 60 * time.Second
DefaultClockSkew is the tolerance applied to exp/nbf checks. Matches the leeway most OIDC providers (Microsoft, Auth0) recommend.
const GoogleIssuer = "https://accounts.google.com"
GoogleIssuer is the canonical OIDC issuer URL for Google-signed ID tokens (Cloud Scheduler, Cloud Functions, etc.).
const GoogleJWKSURL = "https://www.googleapis.com/oauth2/v3/certs"
GoogleJWKSURL is Google's JWKS endpoint. Used as the default when the caller does not override it via SCHEDULED_TASK_OIDC_JWKS_URL.
Variables ¶
var ErrConfigInvalid = errors.New("scheduledauth: invalid configuration")
ErrConfigInvalid signals a fatal configuration parse error. Callers are expected to fail-fast on this — the deploy is misconfigured and the scheduled endpoint would silently accept or reject everything.
ErrUnauthorized is the validation failure returned by Validate. The detailed reason is wrapped via fmt.Errorf so it gets logged but not returned to the client (which only sees 401).
Functions ¶
func IsUnauthorized ¶
IsUnauthorized reports whether err originated from a Validate failure. Useful for callers that need to distinguish auth failures from config errors.
Types ¶
type Config ¶
type Config struct {
Mode Mode
Issuer string
JWKSURL string
Bearer string
Audiences []string
Subjects []string
Skew time.Duration
}
Config holds the parsed configuration for a Validator. NewValidator applies defaults and rejects invalid combinations.
func LoadConfig ¶
LoadConfig parses Config from env. SCHEDULED_TASK_AUTH_MODE MUST be set explicitly to one of "oidc", "bearer", or "disabled" — an unset value returns ErrConfigInvalid so a missed Terraform/Kubernetes env wiring fails server startup instead of silently booting /api/scheduled/* unauthenticated. "disabled" is reserved for an explicit local-dev choice and must be opted into deliberately.
type EnvSource ¶
EnvSource is the minimal interface NewFromEnv depends on. The default implementation (envOS) reads from os.Getenv; tests inject a map.
type Mode ¶
type Mode string
Mode is the authentication mode used for scheduled-task requests.
const ( // ModeOIDC verifies a Google-issued OIDC ID token (RS256 only) on // the Authorization header. Subject pinning is required. ModeOIDC Mode = "oidc" // ModeBearer compares the Authorization header to a shared secret // using crypto/subtle.ConstantTimeCompare. Used on Azure where Logic // Apps fetch the secret from Key Vault at runtime. ModeBearer Mode = "bearer" // ModeDisabled disables auth entirely — every request is allowed // through. Intended for local development only. The validator logs // a WARN at startup and on every request to make this loud. ModeDisabled Mode = "disabled" )
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator authenticates inbound requests against the configured mode.
func New ¶
New builds a Validator from a parsed Config. The caller is responsible for invoking NewFromEnv (or a similar config loader) to populate Config from environment variables.
In oidc mode, the underlying *oidc.RemoteKeySet performs JWKS retrieval lazily on the first request and refreshes on unknown kid (per https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys). Single-flight is built in. There is no fixed TTL — refresh-on- unknown-kid is sufficient because providers always sign new tokens with the new kid before retiring the old key.
To pre-warm the cache and surface JWKS endpoint misconfiguration at startup, callers should invoke (*Validator).Warmup. Failure is logged but does not abort startup — Google's CDN sometimes hiccups and we prefer the validator come up with a stale-on-error fetch on the first real request rather than crashloop the container.
func (*Validator) Middleware ¶
Middleware wraps next with auth enforcement. On failure, writes a 401 with no body (avoiding any implementation-detail leakage) and logs the reason. Successful requests pass through unmodified.
func (*Validator) Validate ¶
Validate is the unit-testable core. It returns nil iff the Authorization header satisfies the configured mode's requirements.
func (*Validator) Warmup ¶
Warmup performs a best-effort sanity check on the JWKS endpoint at startup. Failure is non-fatal — it is logged and the validator stays operable; the underlying *oidc.RemoteKeySet retries on the first real request. This avoids crashlooping the container when Google's CDN hiccups, while still surfacing misconfiguration (wrong URL, blocked egress) prominently in the startup logs.
If ctx has no deadline, a 5s fallback is applied — startup must always make forward progress, even if the caller forgot to bound the warmup. Callers that DO want to wait longer are respected.
Implementation note: go-oidc's RemoteKeySet does not expose a public "fetch now" method — it only fetches on demand from VerifySignature (which requires a parseable JWT). We issue an independent HTTP GET to the JWKS URL to keep the warmup probe lightweight and reusable.