Skip to main content

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:

FlowHelperTemplate
Client credentials (an app logs in with its own id and secret)OAuth2ClientCredentialserp_rest_oauth2_client_credentials, inbound_rest_oauth2_client_credentials
Refresh token (a person authorized the app once)OAuth2RefreshTokenerp_rest_oauth2_refresh_token, inbound_rest_oauth2_refresh_token, zoho_books
JWT signed with a private key (no shared secret)OAuth2JwtBearererp_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​

SecretChangesWho changes it
client_id, client_secretwhen the customer renews the app's secret (Microsoft Entra ID: at least every 24 months)a person, on the connector's Settings & Secrets
refresh_tokenXero, QuickBooks and Exact Online hand out a new one with every access token; Zoho keeps the same onethe connector, with context.update_secret
access_token_cacheevery time a token is fetchedthe connector (TokenCache)
private_keywhen the certificate is reneweda 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.