Lifecycle Webhooks
Lifecycle Events werden von mittwald genutzt, um die externe Anwendung über sie betreffende Ereignisse zu informieren.
Templating von Webhook URLs
Um die URL für die Webhooks zu konfigurieren, können Platzhalter verwendet werden. Diese werden durch die Werte der Attribute des Webhook-Payloads ersetzt.
Folgende Platzhalter können verwendet werden:
apiVersion
: Die API-Version, die für den Webhook verwendet wird. Derzeit immerv1
. Siehe auch API Versioning.contributorId
: ID des ContributorsextensionId
: ID der ExtensionextensionInstanceId
: ID der Extension InstancecontextId
: ID des Extension Contexts, bspw. die ProjectIDcontext
: Der Extension Context Kind, zu dem die Extension Instance hinzugefügt wurde. Derzeit entwedercustomer
oderproject
.
Webhook Payloads
ExtensionAddedToContext
- Schema-Dokumentation
- Beispiel
- JSON Schema
- apiVersionstringrequired
- kind"ExtensionAddedToContext"required
- idstring (uuid)required
- contextobjectrequired
- idstring (uuid)required
- kindstring (one of: customer, project)required
- consentedScopesarray of stringrequired
- Array[
- *string
]
- enabledboolean
- extensionIdstring (uuid)required
- contributorIdstring (uuid)required
- idstring (uuid)required
- createdAtstring (date-time)required
- targetobjectrequired
- methodstringrequired
- urlstring (uri)required
apiVersion: v1
kind: ExtensionAddedToContext
id: d990eb39-041b-40b4-abb9-7a39678a0464
context:
id: f0f86186-0a5a-45b2-aa33-502777496347
kind: customer
consentedScopes:
- mail:read
- mail:write
- domain:read
state:
enabled: true
meta:
extensionId: c593348d-f594-492a-8185-2b89848a4160
contributorId: 680ba069-7465-4932-8b23-e73914b2e051
secret: ROFLieg83rjfdoi9lLIifdlfi399fndLKFHj39fjdlr
request:
id: 018e60ef-ad4d-78d5-97c0-e0405b48ad89
createdAt: 2024-03-14T11:36:24Z
target:
method: POST
url: https://example.com/v1/webhook/...
{
"type": "object",
"required": [
"apiVersion",
"kind",
"id",
"context",
"consentedScopes",
"state",
"meta",
"secret",
"request"
],
"properties": {
"apiVersion": {
"type": "string",
"example": "v1"
},
"kind": {
"type": "string",
"enum": [
"ExtensionAddedToContext"
]
},
"id": {
"type": "string",
"format": "uuid",
"example": "d990eb39-041b-40b4-abb9-7a39678a0464"
},
"context": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"kind": {
"type": "string",
"enum": [
"customer",
"project"
]
}
}
},
"consentedScopes": {
"type": "array",
"example": [
"mail:read",
"mail:write",
"domain:read"
],
"items": {
"type": "string"
}
},
"state": {
"type": "object",
"example": {
"enabled": true
},
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"meta": {
"type": "object",
"required": [
"extensionId",
"contributorId"
],
"properties": {
"extensionId": {
"type": "string",
"format": "uuid",
"example": "c593348d-f594-492a-8185-2b89848a4160"
},
"contributorId": {
"type": "string",
"format": "uuid",
"example": "680ba069-7465-4932-8b23-e73914b2e051"
}
}
},
"secret": {
"type": "string",
"example": "ROFLieg83rjfdoi9lLIifdlfi399fndLKFHj39fjdlr"
},
"request": {
"type": "object",
"required": [
"id",
"createdAt",
"target"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "018e60ef-ad4d-78d5-97c0-e0405b48ad89"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2024-03-14T11:36:24Z"
},
"target": {
"type": "object",
"required": [
"method",
"url"
],
"properties": {
"method": {
"type": "string",
"example": "POST"
},
"url": {
"type": "string",
"format": "uri",
"example": "https://example.com/v1/webhook/..."
}
}
}
}
}
}
}
ExtensionInstanceUpdated
- Schema-Dokumentation
- Beispiel
- JSON Schema
- apiVersionstringrequired
- kind"ExtensionInstanceUpdated"required
- idstring (uuid)required
- contextobjectrequired
- idstring (uuid)required
- kindstring (one of: customer, project)required
- consentedScopesarray of stringrequired
- Array[
- *string
]
- enabledboolean
- extensionIdstring (uuid)required
- contributorIdstring (uuid)required
- idstring (uuid)required
- createdAtstring (date-time)required
- targetobjectrequired
- methodstringrequired
- urlstring (uri)required
apiVersion: v1
kind: ExtensionInstanceUpdated
id: d990eb39-041b-40b4-abb9-7a39678a0464
context:
id: f0f86186-0a5a-45b2-aa33-502777496347
kind: customer
consentedScopes:
- mail:read
- mail:write
- domain:read
state:
enabled: true
meta:
extensionId: c593348d-f594-492a-8185-2b89848a4160
contributorId: 680ba069-7465-4932-8b23-e73914b2e051
request:
id: 018e60ef-ad4d-78d5-97c0-e0405b48ad89
createdAt: 2024-03-14T11:36:24Z
target:
method: POST
url: https://example.com/v1/webhook/...
{
"type": "object",
"required": [
"apiVersion",
"kind",
"id",
"context",
"consentedScopes",
"state",
"meta",
"request"
],
"properties": {
"apiVersion": {
"type": "string",
"example": "v1"
},
"kind": {
"type": "string",
"enum": [
"ExtensionInstanceUpdated"
]
},
"id": {
"type": "string",
"format": "uuid",
"example": "d990eb39-041b-40b4-abb9-7a39678a0464"
},
"context": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"kind": {
"type": "string",
"enum": [
"customer",
"project"
]
}
}
},
"consentedScopes": {
"type": "array",
"example": [
"mail:read",
"mail:write",
"domain:read"
],
"items": {
"type": "string"
}
},
"state": {
"type": "object",
"example": {
"enabled": true
},
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"meta": {
"type": "object",
"required": [
"extensionId",
"contributorId"
],
"properties": {
"extensionId": {
"type": "string",
"format": "uuid",
"example": "c593348d-f594-492a-8185-2b89848a4160"
},
"contributorId": {
"type": "string",
"format": "uuid",
"example": "680ba069-7465-4932-8b23-e73914b2e051"
}
}
},
"request": {
"type": "object",
"required": [
"id",
"createdAt",
"target"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "018e60ef-ad4d-78d5-97c0-e0405b48ad89"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2024-03-14T11:36:24Z"
},
"target": {
"type": "object",
"required": [
"method",
"url"
],
"properties": {
"method": {
"type": "string",
"example": "POST"
},
"url": {
"type": "string",
"format": "uri",
"example": "https://example.com/v1/webhook/..."
}
}
}
}
}
}
}
ExtensionInstanceSecretRotated
- Schema-Dokumentation
- Beispiel
- JSON Schema
- apiVersionstringrequired
- kind"ExtensionInstanceSecretRotated"required
- idstring (uuid)required
- contextobjectrequired
- idstring (uuid)required
- kindstring (one of: customer, project)required
- metaobjectrequired
- extensionIdstring (uuid)required
- contributorIdstring (uuid)required
- secretstringrequired
- requestobjectrequired
- idstring (uuid)required
- createdAtstring (date-time)required
- targetobjectrequired
- methodstringrequired
- urlstring (uri)required
apiVersion: v1
kind: ExtensionInstanceSecretRotated
id: d990eb39-041b-40b4-abb9-7a39678a0464
context:
id: f0f86186-0a5a-45b2-aa33-502777496347
kind: customer
meta:
extensionId: c593348d-f594-492a-8185-2b89848a4160
contributorId: 680ba069-7465-4932-8b23-e73914b2e051
secret: ROFLieg83rjfdoi9lLIifdlfi399fndLKFHj39fjdlr
request:
id: 018e60ef-ad4d-78d5-97c0-e0405b48ad89
createdAt: 2024-03-14T11:36:24Z
target:
method: POST
url: https://example.com/v1/webhook/...
{
"type": "object",
"required": [
"apiVersion",
"context",
"kind",
"id",
"meta",
"secret",
"request"
],
"properties": {
"apiVersion": {
"type": "string",
"example": "v1"
},
"kind": {
"type": "string",
"enum": [
"ExtensionInstanceSecretRotated"
]
},
"id": {
"type": "string",
"format": "uuid",
"example": "d990eb39-041b-40b4-abb9-7a39678a0464"
},
"context": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"kind": {
"type": "string",
"enum": [
"customer",
"project"
]
}
}
},
"meta": {
"type": "object",
"required": [
"extensionId",
"contributorId"
],
"properties": {
"extensionId": {
"type": "string",
"format": "uuid",
"example": "c593348d-f594-492a-8185-2b89848a4160"
},
"contributorId": {
"type": "string",
"format": "uuid",
"example": "680ba069-7465-4932-8b23-e73914b2e051"
}
}
},
"secret": {
"type": "string",
"example": "ROFLieg83rjfdoi9lLIifdlfi399fndLKFHj39fjdlr"
},
"request": {
"type": "object",
"required": [
"id",
"createdAt",
"target"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "018e60ef-ad4d-78d5-97c0-e0405b48ad89"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2024-03-14T11:36:24Z"
},
"target": {
"type": "object",
"required": [
"method",
"url"
],
"properties": {
"method": {
"type": "string",
"example": "POST"
},
"url": {
"type": "string",
"format": "uri",
"example": "https://example.com/v1/webhook/..."
}
}
}
}
}
}
}
ExtensionInstanceRemovedFromContext
- Schema-Dokumentation
- Beispiel
- JSON Schema
- apiVersionstringrequired
- kind"ExtensionInstanceUpdated"required
- idstring (uuid)required
- contextobjectrequired
- idstring (uuid)required
- kindstring (one of: customer, project)required
- consentedScopesarray of stringrequired
- Array[
- *string
]
- enabledboolean
- extensionIdstring (uuid)required
- contributorIdstring (uuid)required
- idstring (uuid)required
- createdAtstring (date-time)required
- targetobjectrequired
- methodstringrequired
- urlstring (uri)required
apiVersion: v1
kind: ExtensionInstanceUpdated
id: d990eb39-041b-40b4-abb9-7a39678a0464
context:
id: f0f86186-0a5a-45b2-aa33-502777496347
kind: customer
consentedScopes:
- mail:read
- mail:write
- domain:read
state:
enabled: true
meta:
extensionId: c593348d-f594-492a-8185-2b89848a4160
contributorId: 680ba069-7465-4932-8b23-e73914b2e051
request:
id: 018e60ef-ad4d-78d5-97c0-e0405b48ad89
createdAt: 2024-03-14T11:36:24Z
target:
method: POST
url: https://example.com/v1/webhook/...
{
"type": "object",
"required": [
"apiVersion",
"kind",
"id",
"context",
"consentedScopes",
"state",
"meta",
"request"
],
"properties": {
"apiVersion": {
"type": "string",
"example": "v1"
},
"kind": {
"type": "string",
"enum": [
"ExtensionInstanceUpdated"
]
},
"id": {
"type": "string",
"format": "uuid",
"example": "d990eb39-041b-40b4-abb9-7a39678a0464"
},
"context": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"kind": {
"type": "string",
"enum": [
"customer",
"project"
]
}
}
},
"consentedScopes": {
"type": "array",
"example": [
"mail:read",
"mail:write",
"domain:read"
],
"items": {
"type": "string"
}
},
"state": {
"type": "object",
"example": {
"enabled": true
},
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"meta": {
"type": "object",
"required": [
"extensionId",
"contributorId"
],
"properties": {
"extensionId": {
"type": "string",
"format": "uuid",
"example": "c593348d-f594-492a-8185-2b89848a4160"
},
"contributorId": {
"type": "string",
"format": "uuid",
"example": "680ba069-7465-4932-8b23-e73914b2e051"
}
}
},
"request": {
"type": "object",
"required": [
"id",
"createdAt",
"target"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "018e60ef-ad4d-78d5-97c0-e0405b48ad89"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2024-03-14T11:36:24Z"
},
"target": {
"type": "object",
"required": [
"method",
"url"
],
"properties": {
"method": {
"type": "string",
"example": "POST"
},
"url": {
"type": "string",
"format": "uri",
"example": "https://example.com/v1/webhook/..."
}
}
}
}
}
}
}
Erläuterung der einzelnen Attribute
apiVersion
Die API-Version, die für den Webhook verwendet wird.
Derzeit immer v1
.
Ermöglicht zukünftige Breaking Changes an dem Payload.
Siehe auch API Versioning.
kind
Art des Lifecycle-Webhooks. Mögliche Werte sind:
ExtensionAddedToContext
ExtensionInstanceUpdated
ExtensionInstanceSecretRotated
ExtensionInstanceRemovedFromContext
Je nach Anforderungen kann dadurch die URL für alle Webhooks identisch sein und die Art des Webhooks anhand des kind
-Attributs unterschieden werden
oder alternativ für jeden Lifecycle ein eigener Webhook-Endpoint konfiguriert und der kind
ignoriert werden.
Für weitere Erläuterungen zu der semantischen Bedeutung der einzelnen Lifecycle-Webhooks siehe Lifecycle Webhooks Konzept.
id
Der global eindeutige Identifier der Extension Instance.
Dies ist nicht zu verwechseln mit der Extension ID, die die Extension als Ganzes identifiziert.
Auch darf diese ID nicht mit der request.id
verwechselt werden, die den Request identifiziert und dafür genutzt werden kann, um Replay-Attacken zu verhindern.
Siehe dazu auch Request Meta Informationen.
context
Informationen über den Extension Context, zu dem die Extension Instance gehört. Derzeit sind zwei Arten von Extension Contexts möglich:
customer
project
Zusätzlich zu dem context.kind
wird auch die context.id
übermittelt.
Dies ist die ID des Extension Contexts, also bspw. des Projektes.
consentedScopes
Die Berechtigungen, denen der Nutzer der Extension beim Hinzufügen zu einem Extension Context zugestimmt hat. Wenn einer Extension nachträglich zusätzliche Scopes hinzugefügt wurden, muss ein Nutzer diesen erst noch zustimmen, bevor die Extension diese für die Extension Instance verwenden kann.
Anhand der consentedScopes
kann die Extension prüfen, ob die benötigten Berechtigungen vorhanden sind.
Deswegen wird empfohlen, die consentedScopes
zu persistieren.
state
Der aktuelle Zustand der Extension Instance.
Über die Lifecycle-Webhooks wird derzeit lediglich über den Zustand von enabled
informiert.
Darüber kann die Extension in Erfahrung bringen, ob die Extension Instance aktiv oder inaktiv ist und somit, ob sie ihre Funktionalität bereitstellen kann.
Für eine deaktivierte Extension Instance funktionieren bspw. die Authentifizierungs- und Autorisierungsmechanismen nicht.
Somit würden auch Domain Actions für eine deaktivierte Extension Instance nicht akzeptiert werden.
meta
Meta-Informationen über die Extension (Instance). Diese beinhalten die IDs der Extension und des Contributors.
Diese Informationen sollten bspw. verwendet werden, um sicherzustellen, dass der Webhook nicht nur ursprünglich von mittwald stammt, sondern auch für die eigene Extension bestimmt ist. Dies beugt Forward-Replay-Attacken vor. Dies ist nicht zu verwechseln mit den Request Meta Informationen, die Aufschluss über den Webhook an sich geben.
secret
Das Secret, das für die Authentifizierung der Extension Instance verwendet wird. Dieses kann verwendet werden, um nutzerunabhängige Authentifizierung zu implementieren. Für weitere Informationen siehe Authentifizierung von Extension Instances.
request
Meta-Informationen über den Lifecycle-Webhook-Request.
Angriffsvektoren, wie Replay- oder Forward Replay Attacks,
können ausgeschlossen werden, indem die request
-Property validiert wird.
Um denselben Request nur einmalig zu akzeptieren, kann die request.id
persistiert und abgeglichen werden.
Das mStudio schickt niemals Requests mit derselben request.id
mehrfach.
Um zu überprüfen, ob der Request den richtigen Empfänger erreicht, kann das Feld request.target.url
überprüft werden.
Dadurch kann verhindert werden, dass ein Angreifer den signierten Request an eine andere Extension weiterleitet.
Um veraltete Requests erkennen zu können, bietet das Feld request.createdAt
einen Anhaltspunkt.
Es sollte jedoch beachtet werden, dass das mStudio die Webhooks potenziell asynchron versendet
und somit die Zeitdifferenz zwischen dem Erstellen des Requests und dem Empfangen des Webhooks variieren kann.
Validierung von Lifecycle Webhooks
Um sicherzustellen, dass der Webhook von mittwald ausging, bieten wir die Möglichkeit, die Identität des Senders und den Inhalt des Request Bodys zu verifizieren. Dazu signieren wir den versendeten Request Body und schicken die Signatur inklusive der Meta-Informationen darüber in Request-Headern mit.
X-Marketplace-Signature-Serial: 7f640dcf-c5fb-4e79-bc4b-99a30e50fcc5
X-Marketplace-Signature-Algorithm: Ed25519
X-Marketplace-Signature: HKdS7xD...qQMN94HINte7Dlof+9V/PQ6gW0C5uBOz+9F6YNOLE6vdb2ybVwRH23GAg==
X-Marketplace-Signature-Serial
Eindeutige ID des zur Überprüfung der Signatur zu nutzenden Public Keys. Wir garantieren für eine Public Key ID einen stabilen Public Key. Dadurch kann der Public Key gecached werden. Ist eine ID unbekannt, kann der Key mit der Operation GET/
über die mittwald-API bezogen werden.
X-Marketplace-Signature-Algorithm
Algorithmus, der zum Erzeugen der Signatur verwendet wurde, derzeit immer Ed25519
.
X-Marketplace-Signature
DSA-Signatur in Base64 codiert.
Um die Signatur zu prüfen, kann eine übliche kryptographische Library in der gewählten Programmiersprache verwendet werden. Dazu wird mithilfe der verify
-Methode der gesamte, unverarbeitete Request Body geprüft. Damit ist sichergestellt, dass der übermittelte Request Body unmodifiziert von mittwald übertragen wurde.
- Go
- PHP
bodyBytes, err := io.ReadAll(body)
if err != nil {
return err
}
if !ed25519.Verify(publicKey, bodyBytes, signature) {
panic("invalid signature")
}
$req = new ExtensionGetPublicKeyRequest($serial);
$resp = $apiClient->marketplace()->extensionGetPublicKey($req);
$key = $resp->getBody()->getKey();
$valid = sodium_crypto_sign_verify_detached(
base64_decode($signature),
$request->getContent(),
base64_decode($key),
);
if (!$valid) {
throw new \Exception('invalid request signature');
}
Referenzimplementierungen für die Validierung von Lifecycle Webhooks
TODO
Dry Running Webhooks
Um die Entwicklung einer Extension zu erleichtern, kann ein Entwickler einer Extension einen Webhook im Dry Run Modus aufrufen lassen.
Bei einer Dry Run webhook execution wird ein Webhook-Kind definiert und anschließend der entprechende Webhook der Extension aufgerufen.
Der Webhook-Endpoint wird hierbei mit Demo-Werten aufgerufen.
Nach dem Dry Run wird das Ergebnis der webhook execution in der APIResponse zurück gegeben.
Inklusive response-body, response-headers und allgemeinen Fehlern.
Um die Dry Run webhook calls zu identifizieren wird in den Query-Parametern der Wert dry-run=true
mitgeliefert.
Ist der dry-run
Query-Parameter auf true
gesetzt, sollte das Extension-Backend diesen Call auch als solchen behandeln und daher den Aufruf als reinen Test interpretieren.
Zudem wird vom mStudio auch der Query-Parameter executing-user-id
gesetzt.
Die extensionInstanceId
hat bei einem Dry Run keinerlei Relevanz für echte Entitäten im mStudio.
Nur Benutzer, die zu einem Contributor gehören dürfen Webhooks als Dry Run ausführen.
Demo-Werte
Die Demo-Werte können als Testdaten genutzt werden, um die interne Logik der Extension zu testen, indem man das folgende JSON-Objekt im Body mit sendet. Sind diese Werte nicht gesetzt, werden sie aus der Extension abgeleitet oder zufällig generiert.
- Schema-Dokumentation
- Beispiel
- JSON Schema
- contextIdstring
- scopesarray of string
- Array[
- *string
]
{
"contextId": "518d4760-93bf-4ec0-9311-7a1343bc26d5",
"scopes": [
"mail:read",
"mail:write",
"domain:read"
],
"instandeDisabled": true,
"createdAt": "2024-03-14T11:36:24Z",
"secret": "<<SECRET>>"
}
{
"type": "object",
"required": [],
"properties": {
"contextId": {
"type": "string",
"example": "518d4760-93bf-4ec0-9311-7a1343bc26d5"
},
"scopes": {
"type": "array",
"example": [
"mail:read",
"mail:write",
"domain:read"
],
"items": {
"type": "string"
}
},
"instandeDisabled": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time",
"example": "2024-03-14T11:36:24Z"
},
"secret": {
"type": "string",
"example": "<<SECRET>>"
}
}
}