OAuth für Apps
Apps anderer Anbieter verbinden sich über OAuth 2.1 mit Bodo: Authorization Code mit PKCE, kurze Access-Tokens, rotierende Refresh-Tokens und Resource Indicators.
Wer entscheidetDer Nutzer stimmt zu, kein Admin gibt Apps frei. Deine App bekommt nie mehr, als der zustimmende Nutzer selbst darf, und nur, wenn die API seiner Organisation an ist. Schaltet die Organisation die API aus, enden alle App-Tokens dort.
Der Ablauf
Zehn Nachrichten zwischen vier Beteiligten. Die Adressen liest deine App aus
/.well-known/oauth-authorization-server.Nutzer im Browser
Deine App
Bodo: Anmeldung und Zustimmung
Bodo API
- Weiterleitung zu
/oauth/authorizemit PKCE (S256) und resource1 - meldet sich an und wählt die Organisation2
- zeigt das Zustimmungsfenster: was die App lesen und ändern will3
- erlaubt4
- zurück zur
redirect_urimit code und state5 POST /oauth/tokenmit code undcode_verifier6- Access-Token (10 min) und Refresh-Token7
GET /v1/…mitAuthorization: Bearer8- Antwort mit den Rechten des Nutzers, geschnitten mit der Zustimmung9
- später, ohne den Nutzer: Refresh-Token verfällt nach 30 Tagen ohne Nutzung, die Zustimmung nach 365 Tagen
- Refresh: neues Refresh-Token, das alte ist ungültig10
- 1Deine App → Bodo: Anmeldung und ZustimmungWeiterleitung zu
/oauth/authorizemit PKCE (S256) und resource - 2Nutzer im Browser → Bodo: Anmeldung und Zustimmungmeldet sich an und wählt die Organisation
- 3Bodo: Anmeldung und Zustimmung → Nutzer im Browserzeigt das Zustimmungsfenster: was die App lesen und ändern will
- 4Nutzer im Browser → Bodo: Anmeldung und Zustimmungerlaubt
- 5Bodo: Anmeldung und Zustimmung → Deine Appzurück zur
redirect_urimit code und state - 6Deine App → Bodo: Anmeldung und Zustimmung
POST /oauth/tokenmit code undcode_verifier - 7Bodo: Anmeldung und Zustimmung → Deine AppAccess-Token (10 min) und Refresh-Token
- 8Deine App → Bodo API
GET /v1/…mitAuthorization: Bearer - 9Bodo API → Deine AppAntwort mit den Rechten des Nutzers, geschnitten mit der Zustimmung
- 10Deine App → Bodo: Anmeldung und ZustimmungRefresh: neues Refresh-Token, das alte ist ungültig
- später, ohne den Nutzer: Refresh-Token verfällt nach 30 Tagen ohne Nutzung, die Zustimmung nach 365 Tagen
1 · App registrieren
In Bodo unter Einstellungen › Schnittstellen › OAuth-Apps ↗: Name, Logo, Redirect-URI. Du bekommst eine
client_id. Öffentliche Clients (Browser, Mobil) brauchen kein Secret, PKCE ist Pflicht.| Feld | Beispiel |
|---|---|
| Name | Muster CRM-Sync |
| Redirect-URI | https://app.musterfirma.de/oauth/callback |
| client_id | app_7Hq2… |
2 · Zur Zustimmung schicken
Mit PKCE (S256) und
resource, damit das Token nur für die API gilt.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.comWas der Nutzer sieht
Muster CRM-Sync möchte auf Bodo zugreifenAngemeldet als Erika Mustermann · Musterfirma GmbH
- LesenKontakte
- ÄndernAufgaben anlegen, ändern, erledigen
- DauerhaftZugriff bleibt, bis du ihn widerrufst (offline_access)
Die App bekommt nie mehr, als du selbst darfst. Widerrufen kannst du jederzeit unter Profil › Verbundene Apps.
AblehnenErlauben
Darunter die Knöpfe Ablehnen und Erlauben.3 · Code gegen Tokens tauschen
Mit dem
code_verifier aus Schritt 2. Das Access-Token gilt 10 Minuten.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 · Erneuern mit Rotation
Jeder Refresh liefert ein neues Refresh-Token, das alte ist danach ungültig.
| Fall | Folge |
|---|---|
| Richtig | neues Refresh-Token sofort speichern, altes verwerfen |
| Wiederverwendung | ein altes Refresh-Token erneut benutzt: Bodo widerruft die ganze Kette, der Nutzer muss neu zustimmen |
| Ablauf | ein Refresh-Token, das 30 Tage unbenutzt bleibt, verfällt; die Zustimmung selbst nach 365 Tagen |
Scopes
Dieselben Rechte wie bei einem Schlüssel, geschnitten mit den Rechten des Nutzers.
| Scope | Erlaubt |
|---|---|
| contacts · companies · tasks · documents | :read oder :write |
| invoices · time_entries | :read oder :write, geldwirksam nur nach KI-Vollmacht des Nutzers |
| webhook_endpoints:write | Webhook-Endpunkte des Nutzers verwalten |
| offline_access | Refresh-Token für Zugriff ohne Anwesenheit |
Ein Token für eine andere resource weist die API mit 401 UNAUTHENTICATED ab. Löschen und Geld-Familien wirken nur, wenn der Nutzer sie einzeln ankreuzt.
Mit dem SDK
Die SDKs bauen PKCE, tauschen den Code und erneuern das Token vor Ablauf. Du speicherst nur das jeweils neue 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);