✅ Verified — used in production at buymywishlist. This plugin has been validated end-to-end in a merged main-branch wfctl.yaml of an active GoCodeAlone project.
Authentication primitives for Workflow applications.
This plugin is marked private in the workflow registry, meaning wfctl plugin install requires a GitHub token with read:packages scope:
export GH_TOKEN=<your-github-personal-access-token>
wfctl plugin install workflow-plugin-authThe plugin binary itself is distributed via public GitHub Releases — GH_TOKEN is only required for the registry lookup step.
auth.credential- WebAuthn/passkey relying-party configuration.
step.auth_passkey_begin_registerstep.auth_passkey_finish_registerstep.auth_passkey_begin_loginstep.auth_passkey_finish_loginstep.auth_totp_generate_secretstep.auth_totp_verifystep.auth_totp_recovery_codesstep.auth_magic_link_generatestep.auth_magic_link_verifystep.auth_magic_link_sendstep.auth_password_hashstep.auth_password_verifystep.auth_challenge_generatestep.auth_challenge_verifystep.auth_normalize_phonestep.auth_methods_policystep.auth_policy_gatestep.auth_methods_responsestep.auth_policy_auditstep.auth_provider_catalogstep.auth_admin_config_describestep.auth_admin_config_validatestep.auth_oauth_provider_configstep.auth_oauth_startstep.auth_oauth_exchangestep.auth_oauth_userinfostep.auth_credential_liststep.auth_credential_revokestep.auth_bootstrap_redeemstep.auth_jwt_issue
step.auth_bootstrap_redeem provides a durable, count-gated first-run admin
code redemption. Bootstrap is OPEN when zero admin credentials exist and CLOSES
permanently once any credential (passkey, google, facebook) is enrolled. The
operator provides a one-time code via the AUTH_BOOTSTRAP_CODE environment
variable (≥16 chars; the step enforces this minimum and returns not_configured
if shorter).
step.auth_jwt_issue mints an HS256 bearer token signed with the shared
AUTH_JWT_SECRET (≥32 chars, matching auth.jwt.Init). The step enforces V-B8:
the standard claims sub, iat, exp, iss, and jti are always written by
the step itself and cannot be overridden via the caller claims map. The minted
token validates directly against an auth.jwt module configured with the same
secret via step.auth_validate.
Typical bootstrap flow:
- Fresh deploy →
GET /admin/bootstrap/statusreturns{open: true}. - Operator redeems the out-of-band code →
POST /admin/bootstrap/redeemwith{code}→step.auth_bootstrap_redeem+step.auth_jwt_issue→{token}. - Super-admin uses the bearer token to enrol a passkey or link SSO.
- First credential inserted → count ≥ 1 → bootstrap closes permanently.
- Re-deploy with same DB → still closed. Empty credential store → re-opens (break-glass).
Environment variables:
AUTH_BOOTSTRAP_CODE— operator-set one-time code, ≥16 characters.AUTH_JWT_SECRET— HS256 signing secret shared withauth.jwtmodule, ≥32 characters.
step.auth_password_hash and step.auth_password_verify are compatibility
steps for non-production auth flows and migrations. Production applications
should use step.auth_methods_policy and step.auth_policy_audit to keep
password auth disabled and detect stored password hashes.
step.auth_challenge_generate emits a six-digit code, code_hash,
normalized destination, channel, and expires_at. The hash is
HMAC-SHA256 over channel, normalized destination, tenant_id, purpose,
and code, using a required signing_secret.
The plugin does not persist challenges. Store code_hash, channel,
destination, tenant_id, purpose, expires_at, attempts, and
max_attempts in the application database. Verify with
step.auth_challenge_verify, then atomically mark the challenge used in app
storage.
step.auth_normalize_phone normalizes US-style phone input to E.164 and passes
through valid E.164 input. It emits generic outputs (valid, phone_e164,
country) plus BMW-compatible aliases (phone, phone_valid).
step.auth_methods_policy computes which auth methods are currently available
from configuration. Missing, empty, templated, or incomplete values disable the
method. Password auth is disabled in production even when requested.
SMS code auth requires routes enabled, SMS enabled, twilio_verify_service_sid,
and either twilio_account_sid plus twilio_auth_token, or
twilio_api_key_sid plus twilio_api_key_secret.
step.auth_policy_gate filters a previous policy step before public auth
conditionals or responses use it. It disables email-code auth unless a concrete
signing_secret is available, filters OAuth providers to supported
implementations (Google by default), and recomputes primary_method_count.
Keep app-specific challenge storage, tenant scoping, identity linking, and JWT
issuance in the consuming app.
step.auth_methods_response converts policy output into a stable response
shape. step.auth_policy_audit reports production password policy violations
for CI or operational checks.
step.auth_admin_config_describe exposes a strict proto contract for admin
portals to render authentication settings. It returns grouped controls with
labels, help text, input types, config keys, disabled reasons, and write-only
secret state. Controls map to real plugin config keys consumed by the auth
policy, OAuth, WebAuthn, challenge, and delivery steps.
step.auth_admin_config_validate accepts a desired config patch and returns a
sanitized accepted patch plus diagnostics. The plugin validates the patch; the
admin host persists accepted config into Workflow configuration or its own
config store. Secret values are never echoed in outputs. Production password
auth, incomplete passkey settings, incomplete OAuth settings, and zero-primary
method configurations are rejected when applicable.
step.auth_provider_catalog merges provider descriptors from auth-provider
plugins. Descriptors advertise provider categories, capabilities, required
config fields, selectable options, admin/app scopes, disabled reasons, and
secret field metadata. step.auth_admin_config_describe consumes these
descriptors so admin portals render provider controls dynamically instead of
hard-coding vendor-specific fields in the admin shell or auth plugin. When no
provider descriptors are supplied, the existing Google/Facebook OAuth controls
remain as a compatibility fallback. Provider capabilities are default-deny:
capabilities must set supported: true before auth admin or policy code treats
them as usable.
OAuth provider config supports Google and Facebook directly as compatibility providers. Additional providers should be supplied by provider descriptors from plugins such as SSO, Okta, Auth0, Entra, Ory, or another provider integration. Policy advertising remains conservative and only marks providers login-ready when the configured provider is supported by the current policy path and every required descriptor field is configured. Instagram, X, and unknown providers return disabled metadata and are not advertised as login-ready unless a provider plugin supplies a real descriptor and implementation.
step.auth_oauth_start emits state, optional PKCE values, return_to,
expires_at, and authorization_url. The plugin does not store OAuth state.
Applications must persist state, code verifier, provider, return path, tenant
or app context, and expiry, then consume the state atomically during callback.
step.auth_oauth_exchange exchanges an authorization code for tokens.
step.auth_oauth_userinfo fetches normalized user claims and emits both
provider_subject and the BMW-compatible provider_user alias.
OAuth endpoint URL overrides are intended for tests. In normal operation,
Google endpoint overrides must remain HTTPS URLs on the expected Google hosts.
Insecure local test endpoints require allow_insecure_test_oauth_endpoints: true.
workflow-plugin-auth is a library of stateless auth primitives; complete auth
flows are composed from these steps plus the engine's built-in auth.* modules
and the provider plugins. Which combination covers which use case:
| Use case | Combination | Demonstrated by |
|---|---|---|
| Same-app session (symmetric) | step.auth_jwt_issue (HS256) → step.auth_validate against an auth.jwt module sharing the secret |
scenario 101 |
| First-run admin bootstrap (durable, passkey/SSO upgrade) | step.auth_bootstrap_redeem (count-gated) + step.auth_jwt_issue + step.auth_passkey_* |
scenario 101 |
| Passkey / passwordless | step.auth_passkey_* (auth.credential module) · step.auth_totp_* · step.auth_magic_link_* |
scenario 101 |
| App-to-app M2M, asymmetric (ES256) — services verify each other with no shared secret | issuer: engine auth.m2m (algorithm: ES256, /oauth/token, /oauth/jwks) → verifier: sso.oidc jwksUri mode + step.sso_validate_token (workflow-plugin-sso ≥ v0.1.8) |
scenario 102 |
| Human / browser login via external IDP (Auth0/Okta/Entra/Ory) | OIDC login (step.auth_oauth_* or engine step.oidc_auth_url/step.oidc_callback) → sso.oidc (discovery mode) + step.sso_validate_token; refresh step.sso_refresh_token; exchange step.sso_token_exchange |
— |
| Enterprise SSO / SCIM | provider plugins (workflow-plugin-{okta,auth0,entra,ory-kratos,ory-hydra,ory-polis,scalekit}) advertised via step.auth_provider_catalog / AuthProviderDescriptor |
— |
| Credential management (list, revoke, delete-min-1) | step.auth_credential_list / step.auth_credential_revoke + consumer db_query/db_exec |
— |
Asymmetric cross-service note (issue #41): the engine's auth.m2m module is
the ES256 issuer + JWKS server (no plugin-side IDP is needed or built). A verifying
app reuses sso.oidc's jwksUri verify-only mode to validate tokens from the
issuer's published JWKS — no shared secret, no OIDC-discovery requirement. External
IDPs plug in through the same provider pattern. See ADR-0002 / ADR-0003.
step.bmw.auth_policy->step.auth_methods_policystep.bmw.auth_policy_gate->step.auth_policy_gatestep.bmw.auth_methods_response->step.auth_methods_responsestep.bmw.oauth_provider_config->step.auth_oauth_provider_configstep.bmw.oauth_start->step.auth_oauth_startstep.bmw.oauth_exchange->step.auth_oauth_exchangestep.bmw.oauth_userinfo->step.auth_oauth_userinfostep.bmw.auth_challenge_generate->step.auth_challenge_generatestep.bmw.auth_challenge_verify->step.auth_challenge_verifystep.bmw.normalize_phone->step.auth_normalize_phone
Keep app-specific SQL, tenant scoping, user identity linking, and JWT issuance in the consuming app until Workflow has a broader identity abstraction.