Skip to main content

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.validateConfig checks 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 are input.config. Without this handler, Owlie checks the install against your schemas instead.
  • connection.testConnection makes 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 a CONFIGURATION_ERROR whose customerMessage tells 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:

StageDeclarative formsUse it for
connectoauth2_authorization_codeBrowser consent. Owlie hosts the callback and exchanges the code.
acquireoauth2_client_credentials, oauth2_jwt_bearer, oauth2_private_key_jwt, oauth2_passwordGetting a token without a browser.
credentialsstatic, oauth2_token_responseTurning inputs or a token response into credentials.
refreshoauth2_refresh_token, and the acquire formsRenewing an expired token.
applyheader, query, basic, base_url, tlsPutting 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"] covers acme.acmeprojects.example;
  • the hosts of literal URLs in your authentication stages (apply.base_url and the token stages' token_url). Owlie derives these itself as egress.auto whenever it builds the connector and ignores any auto entries 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 under manual by 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 your egress domains.
{ "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