Zum Inhalt springen

OAuth für Apps

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.
  1. Deine App → Bodo: Anmeldung und ZustimmungWeiterleitung zu /oauth/authorize mit PKCE (S256) und resource
  2. Nutzer im Browser → Bodo: Anmeldung und Zustimmungmeldet sich an und wählt die Organisation
  3. Bodo: Anmeldung und Zustimmung → Nutzer im Browserzeigt das Zustimmungsfenster: was die App lesen und ändern will
  4. Nutzer im Browser → Bodo: Anmeldung und Zustimmungerlaubt
  5. Bodo: Anmeldung und Zustimmung → Deine Appzurück zur redirect_uri mit code und state
  6. Deine App → Bodo: Anmeldung und ZustimmungPOST /oauth/token mit code und code_verifier
  7. Bodo: Anmeldung und Zustimmung → Deine AppAccess-Token (10 min) und Refresh-Token
  8. Deine App → Bodo APIGET /v1/… mit Authorization: Bearer
  9. Bodo API → Deine AppAntwort mit den Rechten des Nutzers, geschnitten mit der Zustimmung
  10. Deine App → Bodo: Anmeldung und ZustimmungRefresh: neues Refresh-Token, das alte ist ungültig
  11. 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.
FeldBeispiel
NameMuster CRM-Sync
Redirect-URIhttps://app.musterfirma.de/oauth/callback
client_idapp_7Hq2…

2 · Zur Zustimmung schicken

Mit PKCE (S256) und resource, damit das Token nur für die API gilt.
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 · Code gegen Tokens tauschen

Mit dem code_verifier aus Schritt 2. Das Access-Token gilt 10 Minuten.
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 · Erneuern mit Rotation

Jeder Refresh liefert ein neues Refresh-Token, das alte ist danach ungültig.
FallFolge
Richtigneues Refresh-Token sofort speichern, altes verwerfen
Wiederverwendungein altes Refresh-Token erneut benutzt: Bodo widerruft die ganze Kette, der Nutzer muss neu zustimmen
Ablaufein 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.
ScopeErlaubt
contacts · companies · tasks · documents:read oder :write
invoices · time_entries:read oder :write, geldwirksam nur nach KI-Vollmacht des Nutzers
webhook_endpoints:writeWebhook-Endpunkte des Nutzers verwalten
offline_accessRefresh-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);