Authentication and configuration
Building a connector, step 7 (HTTP track, 2 of 5). Previous: Start an HTTP connector.
An install supplies two things: shared configuration (settings such as a subdomain or region) and one authentication option with its credentials. You declare both in the manifest. Authentication is declared, not coded: you describe how to get a token and how to put credentials on each request, and Owlie does it. Code is a last resort for the rare step that needs it.
Configuration
config.schema is a JSON Schema for the settings every authentication option needs. Owlie renders
the install form from it, so give every property a title, a description and, where it helps,
examples. "x-suffix": ".acmeprojects.example" shows a fixed suffix inside the input while storing
only what the admin types. An install is stored as the shared configuration plus the chosen option
and its own configuration, and handlers receive it in that shape:
{
"config": { "subdomain": "acme" },
"auth": { "key": "api-token", "config": { "api_token": "<the token>" } }
}
A handler reads a shared setting as ctx.config.config.subdomain and the chosen option's settings
under ctx.config.auth.config. The shared configuration and an option's configuration must not use
the same property name: Owlie refuses an install where they overlap. Do not declare a property named
egress_domains either; Owlie adds that one itself (below).
Two handlers check configuration:
connection.validateConfigchecks what the schema cannot, such as a malformed domain, and returns{ "valid": false, "errors": [...] }or{ "valid": true }. Its input has the shape above, so the shared settings areinput.config. Without this handler, Owlie checks the install against your schemas instead.connection.testConnectionmakes the cheapest authenticated call that proves both the credentials and the permissions, and returns any value (such as"connected") when it succeeds. When it fails, it throws aCONFIGURATION_ERRORwhosecustomerMessagetells the admin what to fix (Provider errors and quirks).
Authentication options
auth is a list of options. Offer one per way customers actually authenticate. Each option has a
key, a title, its own config.schema for the credentials it needs, and stages:
| Stage | Declarative forms | Use it for |
|---|---|---|
connect | oauth2_authorization_code | Browser consent. Owlie hosts the callback and exchanges the code. |
acquire | oauth2_client_credentials, oauth2_jwt_bearer, oauth2_private_key_jwt, oauth2_password | Getting a token without a browser. |
credentials | static, oauth2_token_response | Turning inputs or a token response into credentials. |
refresh | oauth2_refresh_token, and the acquire forms | Renewing an expired token. |
apply | header, query, basic, base_url, tls | Putting credentials on every request. |
Values are explicit nodes: { "literal": "X" } or { "expr": "'Bearer ' + config.api_token" }.
Inside a stage, an expression reads config (the shared and the option's configuration merged into
one object, unlike a handler's ctx.config), credentials (what the credentials stage produced,
such as credentials.access_token), token_response (the vendor's raw token response) and auth.key
(the option's key). The connect stage sees only config and auth. Operators, functions and more
examples are in Expressions.
An API token
{
"key": "api-token",
"title": "API token",
"config": {
"schema": {
"type": "object",
"required": ["api_token"],
"properties": {
"api_token": {
"type": "string",
"minLength": 1,
"title": "API token",
"format": "password",
"x-sensitive": true
}
},
"additionalProperties": false
}
},
"apply": {
"header": { "Authorization": { "expr": "'Bearer ' + config.api_token" } },
"base_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/api/v2'" }
}
}
OAuth client credentials
{
"key": "client-credentials",
"title": "OAuth client credentials",
"config": {
"schema": {
"type": "object",
"required": ["client_id", "client_secret"],
"properties": {
"client_id": { "type": "string", "minLength": 1, "title": "Client ID" },
"client_secret": {
"type": "string",
"minLength": 1,
"title": "Client secret",
"format": "password",
"x-sensitive": true
}
},
"additionalProperties": false
}
},
"acquire": {
"oauth2_client_credentials": {
"token_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/oauth/token'" },
"client_id": { "expr": "config.client_id" },
"client_secret": { "expr": "config.client_secret" },
"scope": { "literal": "users:read users:write teams:read teams:write" }
}
},
"credentials": { "oauth2_token_response": {} },
"refresh": {
"oauth2_client_credentials": {
"token_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/oauth/token'" },
"client_id": { "expr": "config.client_id" },
"client_secret": { "expr": "config.client_secret" }
}
},
"apply": {
"header": { "Authorization": { "expr": "'Bearer ' + credentials.access_token" } },
"base_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/api/v2'" }
}
}
Every option that gets a token (through acquire, connect or refresh) needs a credentials
stage. The token stages return only the vendor's token response; oauth2_token_response turns it into
credentials.access_token, credentials.refresh_token and an expiry, reading the response's
access_token, refresh_token and expires_in unless you name other keys. Without it,
credentials.access_token stays empty after a token call, and requests go out without a valid
token.
oauth2_client_credentials accepts client_auth (body, the default, sends the client id and
secret as form fields; basic sends them as an HTTP Basic header), a literal grant_type, scope,
audience and extra_params for vendor-specific form fields. The other token forms differ: a
service account that signs a JWT (Google-style domain-wide delegation) is oauth2_jwt_bearer, which
takes scopes and no client_auth. Every stage's fields are in
Connector manifest.
OAuth authorization code
When the vendor needs an admin to consent in a browser, the option has a connect stage. The admin
registers an OAuth app at the vendor, enters its client id and secret in the install, registers the
redirect URL that Owlie shows during connection, and then signs in at the vendor. Owlie hosts the
callback and exchanges the code:
{
"key": "oauth",
"title": "OAuth",
"config": {
"schema": {
"type": "object",
"required": ["client_id", "client_secret"],
"properties": {
"client_id": { "type": "string", "minLength": 1, "title": "Client ID" },
"client_secret": {
"type": "string",
"minLength": 1,
"title": "Client secret",
"format": "password",
"x-sensitive": true
}
},
"additionalProperties": false
}
},
"connect": {
"oauth2_authorization_code": {
"authorize_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/oauth/authorize'" },
"token_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/oauth/token'" },
"client_id": { "expr": "config.client_id" },
"client_secret": { "expr": "config.client_secret" },
"scopes": { "literal": "users:read users:write teams:read teams:write" }
}
},
"credentials": { "oauth2_token_response": {} },
"refresh": {
"oauth2_refresh_token": {
"token_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/oauth/token'" },
"client_id": { "expr": "config.client_id" },
"client_secret": { "expr": "config.client_secret" },
"refresh_token": { "expr": "credentials.refresh_token" }
}
},
"apply": {
"header": { "Authorization": { "expr": "'Bearer ' + credentials.access_token" } },
"base_url": { "expr": "'https://' + config.subdomain + '.acmeprojects.example/api/v2'" }
}
}
scopes is a list, or one string separated by spaces or commas. PKCE is on by default; set
"pkce": { "literal": false } for a vendor that rejects it. response_type defaults to code. When
a refresh response carries no new refresh token, Owlie keeps the previous one.
The base URL
A host that depends on the install (a subdomain, a region, a URL from configuration) is the option's
apply.base_url expression. Put it on every option. Handlers then use paths relative to it
(/users), and never build absolute URLs from configuration.
Secrets
The manifest is copied into every version and every export, so it must never contain a secret. Mark
every secret configuration property with "format": "password" and "x-sensitive": true (or
writeOnly: true). Owlie refuses a manifest that writes a secret as a literal
(auth.<option>.<stage>.<field>.literal_secret) or reads a secret-bearing field from a property that
is not marked secret (auth.<option>.<stage>.<field>.insensitive_reference). The checked fields
include client_secret, private_key, passwords, refresh tokens, and headers or parameters named
like Authorization or api_key. The one allowed literal password is Basic auth with an API key as
the username and the vendor's placeholder password, such as X.
When the vendor refuses a credential with another status
Owlie reads a 401 or 403 as a refused credential, refreshes it and retries once. A vendor that
reports the same thing under another status, or in the body of a 200, needs a reject rule on the
option so Owlie does the same. For example, a vendor that answers 200 with
{ "error": { "code": "invalid_token" } }:
"reject": { "error": { "pointer": "/error/code", "codes": ["invalid_token"] } }
If the second response matches too, the request fails as an AUTH_ERROR and is not retried. Declare
it only when the vendor documents that such a response means the request was not run, because the
request is sent again even when it writes. The fields are in
Connector manifest.
To take an option out of service without deleting it, set "availability": "disabled" on it
(Connector manifest).
When a stage needs code
If a stage genuinely needs code, such as a token endpoint that is not standard OAuth or a request
signature, mark the stage { "custom": true } and export authHooks from lib/auth-hooks.ts. It is
keyed by option key; each option supplies one hook per custom stage (acquire, credentials,
refresh with prepareRefresh, apply). A hook receives the option key, its configuration, the
current credentials, the token response and a fetchImpl for any extra call. A custom stage without
its hook, or a hook no stage uses, is refused. What each hook returns is in
Handler API.
A connector you build cannot combine browser consent (connect) with a custom credentials stage:
Owlie refuses the connection when the admin completes the vendor's sign-in. Use
oauth2_token_response there.
Permissions and scopes
Request exactly what your handlers use. Record, for each Grant kind, the extra scope or admin role its writes need, so a customer who only syncs can grant less. Adding a scope to a published version means existing installs must reconnect to consent to it.
Where the connector may connect
In Owlie's cloud, a custom connector can reach only the hosts it is allowed to, and only over HTTPS. The allowed hosts are:
- the domains in the manifest's
egress.manual. A domain also allows every subdomain of it, so"manual": ["acmeprojects.example"]coversacme.acmeprojects.example; - the hosts of literal URLs in your authentication stages (
apply.base_urland the token stages'token_url). Owlie derives these itself asegress.autowhenever it builds the connector and ignores anyautoentries you write. It cannot read a host out of an expression such as'https://' + config.subdomain + '.acmeprojects.example', so list a host that depends on configuration undermanualby its parent domain; - the hosts an install lists in
egress_domains, a configuration field Owlie adds to the install form of every connector that runs in its cloud, filled in with youregressdomains.
{ "egress": { "manual": ["acmeprojects.example"] } }
In the Connector Builder, add hosts on the Basics tab under Additional allowed domains. A
request to any other host, or over plain http://, fails. Redirects are checked the same way. When a
test run is refused a host, the result lists it under Blocked egress, and Attempted hosts
offers Add to allowlist for each host the handler tried. A handler sees the refusal as a 403
response, which Owlie classifies as AUTH_ERROR after one retry, so a connection test that fails
with an authentication error on a new connector often means a missing domain.
Test runs in the builder are stricter than published runs. A test reaches only the exact hosts listed
above Run in the Test tab, not their subdomains, and it evaluates an authentication
base_url or token_url expression over your saved Run config, so
'https://' + config.subdomain + '.acmeprojects.example' becomes the one host acme.acmeprojects.example
(Test your connector). A published connector and its installs still use the rules above.
Next: Read handlers