OAuth 2.0
Most ERPs and SaaS APIs use OAuth 2. The helpers cover the three flows you meet in practice; each has a template:
| Flow | Helper | Template |
|---|---|---|
| Client credentials (an app logs in with its own id and secret) | OAuth2ClientCredentials | erp_rest_oauth2_client_credentials, inbound_rest_oauth2_client_credentials |
| Refresh token (a person authorized the app once) | OAuth2RefreshToken | erp_rest_oauth2_refresh_token, inbound_rest_oauth2_refresh_token, zoho_books |
| JWT signed with a private key (no shared secret) | OAuth2JwtBearer | erp_rest_oauth2_jwt |
All three plug into HttpClient(auth=...). The client asks for a token
before the first call, uses it until it expires, and on a 401 fetches a new
one and repeats the call once.
What is stored, what rotates
| Secret | Changes | Who changes it |
|---|---|---|
client_id, client_secret | when the customer renews the app's secret (Microsoft Entra ID: at least every 24 months) | a person, on the connector's Settings & Secrets |
refresh_token | Xero, QuickBooks and Exact Online hand out a new one with every access token; Zoho keeps the same one | the connector, with context.update_secret |
access_token_cache | every time a token is fetched | the connector (TokenCache) |
private_key | when the certificate is renewed | a person |
The connector may only write a secret that is listed under Secrets the
connector may update on the connector's Settings & Secrets tab
(rotatable_secrets in a toolkit settings file). The templates list theirs.
Client credentials
from lib.helpers import Settings, HttpClient, OAuth2ClientCredentials, TokenCache, expiry_warning
def connect(context, summary):
s = Settings(context)
expiry_warning(context, 'client_secret_expires_on', summary=summary) # warns 30 days ahead
auth = OAuth2ClientCredentials(
s.str('token_url'), s.secret('client_id'), s.secret('client_secret'),
scope=s.str('scope', None), # e.g. https://api.businesscentral.dynamics.com/.default
client_auth=s.choice('client_auth', ['body', 'basic'], 'body'),
cache=TokenCache(context))
return HttpClient(s.str('base_url'), auth=auth, context=context, name='ERP')
TokenCache keeps the access token between runs in the secret
access_token_cache, so ten entrypoints running in the same hour fetch one
token instead of ten. Without it in the rotatable list the token is kept for
the run only (a warning says so).
client_secret_expires_on is a setting with the secret's expiry date
(2027-03-31). From 30 days before, the run log warns, and so does the run
result when the run's RunSummary is passed. The customer renews the secret;
you paste the new one and the new date.
Refresh token
auth = OAuth2RefreshToken(context, s.str('token_url'), s.secret('client_id'),
s.secret('client_secret', None), refresh_token_secret='refresh_token',
cache=TokenCache(context))
When the token endpoint answers with a new refresh token, the helper stores
it at once with context.update_secret('refresh_token', ...), before the run
does anything else, and masks it in the log. The old one is usually dead from
that moment, so a run that crashed later must not lose it: that is why it is
stored first.
Getting the first refresh token is a one-time authorization by a person at
the customer (the authorization-code flow in a browser). Paste the result in
the refresh_token secret. If the token is revoked or unused too long, the
run fails with the provider's invalid_grant; authorize again.
JWT with a private key
auth = OAuth2JwtBearer(s.str('token_url'), s.pem('private_key'),
issuer=s.str('jwt_issuer'), subject=s.str('jwt_subject', None),
audience=s.str('jwt_audience', None), key_id=s.str('jwt_key_id', None),
cache=TokenCache(context))
Nothing secret travels: the connector signs a short-lived JWT and trades it
for an access token. The template supports the assertion grant (Google,
Salesforce) and private-key client authentication (Microsoft Entra ID with a
certificate). Paste the private key in the secret; a key pasted into the
one-line field loses its line breaks, and s.pem() rebuilds them.
Scopes
Ask for the least the connector needs: read for master data, write for the invoice endpoints. A 403 from the ERP with a valid token usually means a missing scope or a missing role for the app's user in the ERP.
Testing locally
In the toolkit, put the secrets in the settings file and list
refresh_token (and access_token_cache) under rotatable_secrets. The
toolkit writes a rotated value back into the settings file, as production
does in the secret store, and reads the file again before every run.
Common pitfalls
- Refresh token lost after a rotation: the secret was not in the rotatable list, so the new value could not be stored. The run fails with a message naming the secret; add the name and authorize again.
- Token endpoint rate limits: without
TokenCache, every run (and every entrypoint) fetches a token. Zoho allows only a few per refresh token in ten minutes. - Clock: a JWT is refused when its issue time is in the future; the container clock is synchronized, a local PC may not be.