Skip to content

OAuth for apps

OAuth for apps

Apps of other vendors connect to Bodo through OAuth 2.1: authorization code with PKCE, short access tokens, rotating refresh tokens and resource indicators.
Who decidesThe user consents, no admin approves apps. Your app never gets more than the consenting user may do, and only while the API of their organization is on. When the organization switches the API off, all app tokens there end.

The flow

Ten messages between four parties. Your app reads the addresses from /.well-known/oauth-authorization-server.
  1. Your app → Bodo: sign-in and consentredirect to /oauth/authorize with PKCE (S256) and resource
  2. User in the browser → Bodo: sign-in and consentsigns in and picks the organization
  3. Bodo: sign-in and consent → User in the browsershows the consent window: what the app wants to read and change
  4. User in the browser → Bodo: sign-in and consentallows
  5. Bodo: sign-in and consent → Your appback to the redirect_uri with code and state
  6. Your app → Bodo: sign-in and consentPOST /oauth/token with code and code_verifier
  7. Bodo: sign-in and consent → Your appaccess token (10 min) and refresh token
  8. Your app → Bodo APIGET /v1/… with Authorization: Bearer
  9. Bodo API → Your appanswer with the user’s rights, intersected with the consent
  10. Your app → Bodo: sign-in and consentrefresh: a new refresh token, the old one is invalid
  11. later, without the user: the refresh token expires after 30 days unused, the consent after 365 days

1 · Register the app

In Bodo under Settings › Interfaces › OAuth apps: name, logo, redirect URI. You get a client_id. Public clients (browser, mobile) need no secret, PKCE is required.
FieldExample
NameMuster CRM-Sync
Redirect URIhttps://app.musterfirma.de/oauth/callback
client_idapp_7Hq2…

2 · Send the user to consent

With PKCE (S256) and resource, so the token is only valid for the API.
HTTP
GET https://api.bodo-app.com/oauth/authorize
  ?response_type=code
  &client_id=app_7Hq2…
  &redirect_uri=https://app.musterfirma.de/oauth/callback
  &scope=contacts:read tasks:write offline_access
  &state=9f2c…
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &resource=https://api.bodo-app.com

3 · Trade the code for tokens

With the code_verifier from step 2. The access token is valid for 10 minutes.
HTTP
POST https://api.bodo-app.com/oauth/token
grant_type=authorization_code&code=…&code_verifier=…
&client_id=app_7Hq2…&resource=https://api.bodo-app.com

{
  "access_token": "eyJhbGciOiJFUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "bodo_rt_8Vd1…",
  "scope": "contacts:read tasks:write offline_access"
}

4 · Refresh with rotation

Every refresh returns a new refresh token; the old one is invalid from then on.
CaseConsequence
Correctstore the new refresh token at once, drop the old one
Reusean old refresh token used again: Bodo revokes the whole chain, the user has to consent again
Expirya refresh token unused for 30 days expires; the consent itself after 365 days

Scopes

The same rights as for a key, intersected with the rights of the user.
ScopeAllows
contacts · companies · tasks · documents:read or :write
invoices · time_entries:read or :write, money actions only under the user’s AI mandate
webhook_endpoints:writemanage the user’s webhook endpoints
offline_accessrefresh token for access while the user is away

The API rejects a token for another resource with 401 UNAUTHENTICATED. Deleting and the money families only work when the user ticks them one by one.

With the SDK

The SDKs build PKCE, trade the code and refresh the token before it expires. You only store the latest refresh token.
// Portal D12 · OAuth — PKCE, consent, code exchange and refresh with rotation.
// Run: BODO_CLIENT_ID=bodo_ci_… BODO_CLIENT_SECRET=… bun examples/oauth.ts
import { Bodo } from "@bodo/api";
import {
  buildAuthorizeUrl,
  createPkcePair,
  exchangeCode,
  OAuthSession,
  OFFLINE_ACCESS_SCOPE,
} from "@bodo/api/oauth";

const clientId = process.env.BODO_CLIENT_ID ?? "";
const clientSecret = process.env.BODO_CLIENT_SECRET;
const redirectUri = "https://crm.musterfirma.de/oauth/callback";

// Step 2 · send the user to the consent page
const pkce = await createPkcePair();
const state = crypto.randomUUID();
console.log(
  buildAuthorizeUrl({
    clientId,
    redirectUri,
    scope: ["contacts:read", "contacts:write", OFFLINE_ACCESS_SCOPE],
    state,
    codeChallenge: pkce.challenge,
  }),
);

// Step 3 · your callback receives ?code=…&state=… — compare state, then trade the code
const code = process.env.BODO_OAUTH_CODE ?? "";
const tokens = await exchangeCode({
  clientId,
  clientSecret,
  code,
  codeVerifier: pkce.verifier,
  redirectUri,
});

// Step 4 · one session per grant: it refreshes before expiry and rotates the refresh token
const session = new OAuthSession({
  clientId,
  clientSecret,
  tokens,
  onTokens: (next) => {
    // persist next.refreshToken — the previous one is invalid from now on
    console.log("rotated, valid until", new Date(next.expiresAt).toISOString());
  },
});

const bodo = new Bodo({ oauth: session });
const me = await bodo.me();
console.log(me.authMethod, me.principalType);