Factor makes it easy to run twelve-factor apps locally and in CI.
factor aims to provide a local version of the facilities normally provided by a
twelve factor platform, such as routing, and explore proposals for new
twelve factor recommendations, such as workload identity.
Note
It also serves as a place to experiment with new twelve-factor recommendations, because the ability to provide a local version of a proposed recommendation is a great way to verify that the recommendation is not overly coupled to the implementation details of specific platforms.
While updating twelve-factor to clarify the contract between the application and the platform, it became clear that introducing additional platform features like workload identity would require a way to run those features locally. This repository holds an experimental api for replicating the features of a twelve-factor platform locally.
This cli will evolve and change over time. The factor cli aims to:
- Provide an easy way to run twelve-factor apps locally.
- Provide a way to run twelve factor apps in CI. Ideally,
factorcould use the app'sproject.tomlconfiguration to run the CI suite in an environment as close as possible to the production environment. - Act as a "polyfill" mechanism to fill in gaps in features provided by the platform. This allows twelve-factor apps running on platforms that do not yet support all of the twelve-factor features to work.
- Be a place to experiment with new twelve-factor concepts to clarify recommendations for the manifesto.
Factor intends to explore whether devcontainers can work with Cloud Native Buildpacks to help create a local dev environment that is a close match to the production environment without configuration.
Factor does not intend to be a general-purpose "Serverless" framework or a general-purpose abstraction over multiple platforms.
Instead, it remains focused on providing the facilities expected by a twelve factor platform outside of the platform: for local development, testing and CI.
The current iteration is focused primarily on workload identity generation and validation. In the future we plan to experiment more with extensions to port binding, constellations of twelve-factor apps, connections between apps and backing services, and consistent local builds.
The CLI supports:
- exposing app remotely via ngrok
.envfile loading and change detection- workload identity managed with a local oidc provider, auth0, or k8s
- incoming identity validation via proxy
To begin, copy the example.factor to your home directory:
cp example.factor ~/.factorAdd a secret to your local identity provider:
$ echo secret = "\"$(openssl rand -base64 32)\"" >> ~/.factorCreate an app:
$ factor create --app localAdd an identity to your app:
$ echo DEFAULT_AUDIENCE=default >> .envRun an echo server:
$ factor run ./echo.sh --incoming-identity example-clients.jsonIn another terminal window, load the file and make a request passing in the token:
$ TOKEN=$(cat DEFAULT.token); \
curl -v -H "Authorization: Bearer $TOKEN" http://localhost:5000You should see X-Client-Id: DEFAULT in the response from the server.
Note that in this case we are generating an oidc token and validating it
locally.
Factor CLI acts as a platform that provides services to your application in the same way a cloud platform would. This platform is configured through environment variables and provides services through environment variables and headers.
On the client side, Factor acts as an identity provider that generates tokens for your application to use when calling other services:
Configuration (what your application provides to Factor):
*_AUDIENCEenvironment variables - Define the audiences for which Factor should generate tokens- Example:
DEFAULT_AUDIENCE=api.example.comwill generate a token for the audience "api.example.com"
- Example:
Services (what Factor provides to your application):
*_CREDSenvironment variables - Contain credentials that your application can use to authenticate to other services- Example:
DEFAULT_CREDS='{"type":"oidc","data":{"token":"file:///path/to/DEFAULT.token"}}'
- Example:
- Token files - Written to disk and referenced by the
*_CREDSvariables- These files are automatically updated by Factor (typically every 15 minutes)
- Applications should monitor and reload these files when they change to maintain valid credentials
On the server side, Factor acts as an identity-aware proxy that validates incoming tokens and passes identity information to your application:
Configuration (what your application provides to Factor):
*_CLIENT_CREDSenvironment variables - Define the client identities that are allowed to access your application- Example:
SERVICE_A_CLIENT_CREDS='{"type":"oidc","data":{"iss":"https://auth\\.example\\.com","sub":"service-.*","aud":"api"}}'
- Example:
REJECT_UNKNOWNenvironment variable - When set to "true", rejects requests from unidentified clients- When not set or "false", requests without valid tokens will pass through without the
X-Client-Idheader
- When not set or "false", requests without valid tokens will pass through without the
Services (what Factor provides to your application):
X-Client-Idheader - Contains the client ID of the authenticated caller, validated from their token- Example: When a request comes in with a valid token, your application receives
X-Client-Id: SERVICE_A
- Example: When a request comes in with a valid token, your application receives
This separation of concerns allows your application to focus on its business logic while Factor handles the complexities of identity management, token validation, and proxy configuration.
This project follows the main twelve-factor governance including the code of conduct defined there. Because it is experimental, it does not use the same change management or major release guidelines. Please treat it as alpha-quality software.
factor create
This creates the config for a local app and stores it in .factor-app or the
specified config file via --config.
factor run
Loads an .env file if available, starts the given subcommand and proxies
requests to the subcommand. Validates incoming bearer tokens using the same
mechanism as proxy and syncs ids using the same mechanism as id. Audiences
can also be specified by setting CLIENT_ID_AUDIENCE=<audience> in environment
variables. Token files will be created in the current working directory.
The application will receive credentials through environment variables using the *_CREDS suffix format:
{
"type": "oidc",
"data": {
"token": "file:///path/to/token/file"
}
}For example:
# The environment variable name before _CREDS becomes the credential name
export SERVICE_CREDS='{"type":"oidc","data":{"token":"file:///tmp/service.token"}}'Additionally, if an ngrok key is specified in the app config, it will start an
ngrok tunnel and forward requests to the application. The ngrok url is
available to the application in the env var NGROK_URL.
factor id
Starts a background process to write and update one workload identity file for
each audience specified. The file will be stored in ./. This can be updated
by specifying --path on the command line or changing the path value in
.factor-app.
factor proxy
Listens on --port and proxies requests to --child_port. Validates incoming
bearer tokens based on the flag --incoming-identity or INCOMING_IDENTITY in
the environment. Identity data can be provided in two ways:
- As a JSON/TOML file specified by
--incoming-identity:
{
"<client_id_a>": {
"iss": "<issuer_regex>",
"sub": "<subject_regex>",
"aud": "<audience_regex>"
},
"<client_id_b>": {
...
},
...
}- Through environment variables using the
*_CLIENT_CREDSformat:
{
"type": "oidc",
"client_id": "optional_override",
"data": {
"iss": "<issuer_regex>",
"sub": "<subject_regex>",
"aud": "<audience_regex>"
}
}For example:
# The client_id field is optional - if omitted, the environment variable name without _CLIENT_CREDS is used
export SERVICE_A_CLIENT_CREDS='{"type":"oidc","data":{"iss":"https://auth\\.example\\.com","sub":"service-.*","aud":"api"}}'If a matching token is supplied, then the header X-Client-Id will be
set to the value of the matching client id. To reject requests that don't
match, use the flag --reject-unknown
factor info
Prints out info for the current application. If factor is already running it will dynamically print out the current data, otherwise loads the identity provider to determine the values.
The output will be something like:
name=local
url=http://localhost:5000
iss=http://localhost:5000
sub=local