From de98475ddb3d3a37177577aabd9c1fa23a246c4f Mon Sep 17 00:00:00 2001 From: Yufei Cai Date: Thu, 4 Nov 2021 12:58:05 +0100 Subject: [PATCH] Document OAuth2 client credentials flow for HTTP connections. Signed-off-by: Yufei Cai --- .../src/main/resources/connectivity.conf | 4 + .../resources/_posts/2021-11-03-oauth2.md | 211 ++++++++++++++++++ .../main/resources/jsonschema/connection.json | 45 ++++ .../connectivity-protocol-bindings-http.md | 64 +++++- 4 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 documentation/src/main/resources/_posts/2021-11-03-oauth2.md diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index d0de7f4f19..866aec3cfa 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -331,8 +331,12 @@ ditto { } oauth2 { + # Maximum clock skew of OAuth2 token endpoints. Access tokens are refreshed this long before expiration. max-clock-skew = 60s max-clock-skew = ${?CONNECTIVITY_HTTP_OAUTH2_MAX_CLOCK_SKEW} + + # Whether to enforce HTTPS for OAuth2 token endpoints. Should be `true` for production environments + # in order not to transmit client secrets in plain text. enforce-https = true enforce-https = ${?CONNECTIVITY_HTTP_OAUTH2_ENFORCE_HTTPS} } diff --git a/documentation/src/main/resources/_posts/2021-11-03-oauth2.md b/documentation/src/main/resources/_posts/2021-11-03-oauth2.md new file mode 100644 index 0000000000..b524f054bf --- /dev/null +++ b/documentation/src/main/resources/_posts/2021-11-03-oauth2.md @@ -0,0 +1,211 @@ +--- +title: "Support for OAuth2 client credentials flow for HTTP connections" +published: true +permalink: 2021-11-03-oauth2.html +layout: post +author: yufei_cai +tags: [blog, architecture, connectivity] +hide_sidebar: true +sidebar: false +toc: true +--- + +The upcoming release of Eclipse Ditto **version 2.2.0** supports HTTP connections that authenticate their requests +via OAuth2 client credentials flow as described in +[section 4.4 of RFC-6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). + +Detailed information can be found at +[Connectivity API > HTTP 1.1 protocol binding](connectivity-protocol-bindings-http.html#oauth2-client-credentials-flow). + +This blog post shows an example of publishing a twin event to an HTTP endpoint via OAuth2 client credentials flow. +For simplicity, we will use `webhook.site` for both the token endpoint and the event publishing destination. +Feel free to substitute them for real OAuth and HTTP servers. + +# Prerequisites + +This example requires 2 webhooks. We will use +- `https://webhook.site/785e80cd-e6e6-452a-be97-a59c53edb4d9` for access token requests, and +- `https://webhook.site/6148b899-736f-47e6-9382-90b1d721630e` for event publishing. + +Replace the webhook URIs by your own. + +# Configure the token endpoint + +Configure the token webhook to return a valid access token response. Here is an example for a token expiring +at 00:00 on 1 January 3000. The field `expires_in` is an arbitrary big number not reflecting the actual expiration +time of the access token. + +- Status code: 200 +- Content type: `application/json` +- Response body: + ```json + { + "access_token": "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9.ewogICJhdWQiOiBbXSwKICAiY2xpZW50X2lkIjogIm15LWNsaWVudC1pZCIsCiAgImV4cCI6IDMyNTAzNjgwMDAwLAogICJleHQiOiB7fSwKICAiaWF0IjogMCwKICAiaXNzIjogImh0dHBzOi8vbG9jYWxob3N0LyIsCiAgImp0aSI6ICI3ODVlODBjZC1lNmU2LTQ1MmEtYmU5Ny1hNTljNTNlZGI0ZDkiLAogICJuYmYiOiAwLAogICJzY3AiOiBbCiAgICAibXktc2NvcGUiCiAgXSwKICAic3ViIjogIm15LXN1YmplY3QiCn0.QUJD", + "expires_in": 1048576, + "scope": "my-scope", + "token_type": "bearer" + } + ``` + +The access token has the form `..`, where `` and `` are base64-encoding +of the headers and the body in JSON format, and `` is the base-64 encoded signature computed according +to the issuer's key pair. Since the token webhook is not a real OAuth2 server, the signature in the example is a +placeholder. The unencoded headers and body are as follows. + +### Headers + +```json +{ + "alg": "RS256", + "typ": "JWT" +} +``` + +### Body + +```json +{ + "aud": [], + "client_id": "my-client-id", + "exp": 32503680000, + "ext": {}, + "iat": 0, + "iss": "https://localhost/", + "jti": "785e80cd-e6e6-452a-be97-a59c53edb4d9", + "nbf": 0, + "scp": [ + "my-scope" + ], + "sub": "my-subject" +} +``` + +# Create the connection + +[Create a connection](connectivity-manage-connections.html#create-connection) +publishing twin events to the event publishing webhook using OAuth2 credentials. +The `tokenEndpoint` field is set to the access token webhook. + +```json +{ + "id": "http_oauth2", + "name": "http_oauth2", + "connectionType": "http-push", + "connectionStatus": "open", + "uri": "https://webhook.site:443", + "sources": [], + "targets": [ + { + "address": "POST:/6148b899-736f-47e6-9382-90b1d721630e", + "topics": ["_/_/things/twin/events"], + "authorizationContext": ["integration:ditto"], + "headerMapping": {} + } + ], + "clientCount": 1, + "processorPoolSize": 1, + "failoverEnabled": true, + "validateCertificates": true, + "specificConfig": {}, + "tags": [], + "credentials": { + "type": "oauth-client-credentials", + "tokenEndpoint": "https://webhook.site/785e80cd-e6e6-452a-be97-a59c53edb4d9", + "clientId": "my-client-id", + "clientSecret": "my-client-secret", + "requestedScopes": "my-scope" + } +} +``` + +# Generate a thing-created event + +[Create a thing](http-api-doc.html#/Things/post_things) +granting read access to the connection's subject. The thing-created event will be distributed +to the connection for publishing. + +```json +{ + "_policy": { + "entries": { + "DEFAULT": { + "subjects": { +{%raw%} "{{ request:subjectId }}"{%endraw%}: { + "type": "the creator" + }, + "integration:ditto": { + "type": "the connection" + } + }, + "resources": { + "policy:/": { + "grant": ["READ", "WRITE"], + "revoke": [] + }, + "thing:/": { + "grant": ["READ", "WRITE"], + "revoke": [] + } + } + } + } + } +} +``` + +# HTTP requests made by the HTTP connection + +Before the HTTP connection publishes the thing-created event, it makes an access token request against the token +endpoint to obtain a bearer token. + +``` +POST /785e80cd-e6e6-452a-be97-a59c53edb4d9 HTTP/1.1 +Host: webhook.site +Accept: application/json +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials +&client_id=my-client-id +&client_secret=my-client-secret +&scope=my-scope +``` + +The request should appear at the access token webhook. The webhook should return the configured access token response. + +```json +{ + "access_token": "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9.ewogICJhdWQiOiBbXSwKICAiY2xpZW50X2lkIjogIm15LWNsaWVudC1pZCIsCiAgImV4cCI6IDMyNTAzNjgwMDAwLAogICJleHQiOiB7fSwKICAiaWF0IjogMCwKICAiaXNzIjogImh0dHBzOi8vbG9jYWxob3N0LyIsCiAgImp0aSI6ICI3ODVlODBjZC1lNmU2LTQ1MmEtYmU5Ny1hNTljNTNlZGI0ZDkiLAogICJuYmYiOiAwLAogICJzY3AiOiBbCiAgICAibXktc2NvcGUiCiAgXSwKICAic3ViIjogIm15LXN1YmplY3QiCn0.QUJD", + "expires_in": 1048576, + "scope": "my-scope", + "token_type": "bearer" +} +``` + +The HTTP connection will cache the access token and use it to authenticate itself at the event publishing webhook +for each thing event, including the first thing-created event. + +``` +POST /6148b899-736f-47e6-9382-90b1d721630e HTTP/1.1 +Host: webhook.site +Content-Type: application/vnd.eclipse.ditto+json +Authorization: Bearer ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9.ewogICJhdWQiOiBbXSwKICAiY2xpZW50X2lkIjogIm15LWNsaWVudC1pZCIsCiAgImV4cCI6IDMyNTAzNjgwMDAwLAogICJleHQiOiB7fSwKICAiaWF0IjogMCwKICAiaXNzIjogImh0dHBzOi8vbG9jYWxob3N0LyIsCiAgImp0aSI6ICI3ODVlODBjZC1lNmU2LTQ1MmEtYmU5Ny1hNTljNTNlZGI0ZDkiLAogICJuYmYiOiAwLAogICJzY3AiOiBbCiAgICAibXktc2NvcGUiCiAgXSwKICAic3ViIjogIm15LXN1YmplY3QiCn0.QUJD + +{ + "topic": "//things/twin/events/created", + "headers": {}, + "path": "/", + "value": { + "policyId": "" + }, + "revision": 1 +} +``` + +The HTTP connection will obtain a new token from the access token webhook when the previous token is about to expire. + +Please [get in touch](feedback.html) if you have feedback or questions regarding this new functionality. +
+
+{% include image.html file="ditto.svg" alt="Ditto" max-width=500 %} +--
+The Eclipse Ditto team diff --git a/documentation/src/main/resources/jsonschema/connection.json b/documentation/src/main/resources/jsonschema/connection.json index bf7337d67e..0ae8d3f590 100644 --- a/documentation/src/main/resources/jsonschema/connection.json +++ b/documentation/src/main/resources/jsonschema/connection.json @@ -300,6 +300,51 @@ } } } + }, + { + "$id": "/properties/credentials#oauth2-client-credentials", + "type": "object", + "title": "OAuth2 client credentials", + "description": "Credentials for OAuth2 client credentials flow over HTTP", + "properties": { + "type": { + "$id": "/properties/credentials/properties/type#oauth2-client-credentials", + "type": "string", + "enum": [ + "oauth-client-credentials" + ], + "title": "Type of credentials", + "description": "Type of credentials", + "examples": [ + "oauth-client-credentials" + ] + }, + "tokenEndpoint": { + "$id": "/properties/credentials/properties/tokenEndpoint#oauth2-client-credentials", + "type": "string", + "title": "Token endpoint", + "description": "URI of the token endpoint", + "examples": ["https://oauth2-provider.example.com/token"] + }, + "clientId": { + "$id": "/properties/credentials/properties/clientId#oauth2-client-credentials", + "type": "string", + "title": "Client ID", + "description": "Client ID to include in access token requests" + }, + "clientSecret": { + "$id": "/properties/credentials/properties/clientSecret#oauth2-client-credentials", + "type": "string", + "title": "Client secret", + "description": "Client secret to include in access token requests" + }, + "requestedScopes": { + "$id": "/properties/credentials/properties/requestedScopes#oauth2-client-credentials", + "type": "string", + "title": "Requested scopes", + "description": "Space-separated requested scopes to include in access token requests" + } + } } ] }, diff --git a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-http.md b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-http.md index 00c28cca5d..c9d65badf0 100644 --- a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-http.md +++ b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-http.md @@ -192,7 +192,7 @@ Here is an example HTTP connection that checks the server certificate and authen "connectionType": "http-push", "connectionStatus": "open", "failoverEnabled": true, - "uri": "https://localhost:80", + "uri": "https://localhost:443", "validateCertificates": true, "ca": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----", "credentials": { @@ -225,3 +225,65 @@ Here is an example HTTP connection that checks the server certificate and authen Ditto supports HMAC request signing for HTTP push connections. Find detailed information on this in [Connectivity API > HMAC request signing](connectivity-hmac-signing.html). + +### OAuth2 client credentials flow + +HTTP push connections can authenticate themselves via OAuth2 client credentials flow as described in +[section 4.4 of RFC-6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). +To configure OAuth2 credentials: +- Set `type` to `oauth-client-credentials` +- Set `tokenEndpoint` to the URI of the access token request endpoint +- Set `clientId` to the client ID to include in access token requests +- Set `clientSecret` to the client secret to include in access token requests +- Set `requestedScopes` to the scopes to request in access token requests + +This is an example connection with OAuth2 credentials. +```json +{ + "connection": { + "id": "http-example-connection-124", + "connectionType": "http-push", + "connectionStatus": "open", + "failoverEnabled": true, + "uri": "https://localhost:443/event-publication", + "credentials": { + "type": "oauth-client-credentials", + "tokenEndpoint": "https://localhost:443/oauth2/token", + "clientId": "my-client-id", + "clientSecret": "my-client-secret", + "requestedScopes": "user-scope-1 role-scope-2" + }, + ... +} +``` + +Each HTTP request to `https://localhost:443/event-publication` includes a bearer token issued by +`https://localhost:443/oauth2/token`. The HTTP connection will obtain a new token before the old token expires +according to a configured `max-clock-skew`. To prevent looping access token requests, each token is used once even if +the token endpoint responds with expired tokens. Rejected or malformed access token responses are considered +misconfiguration errors. + +It is possible to configure `max-clock-skew` and whether to enforce HTTPS for token endpoints in +`connectivity-extension.conf` or by environment variables. +```hocon +ditto { + connectivity { + connection { + http-push { + oauth2 { + # Maximum clock skew of OAuth2 token endpoints. + # Access tokens are refreshed this long before expiration. + max-clock-skew = 60s + max-clock-skew = ${?CONNECTIVITY_HTTP_OAUTH2_MAX_CLOCK_SKEW} + + # Whether to enforce HTTPS for OAuth2 token endpoints. + # Should be `true` for production environments + # in order not to transmit client secrets in plain text. + enforce-https = true + enforce-https = ${?CONNECTIVITY_HTTP_OAUTH2_ENFORCE_HTTPS} + } + } + } + } +} +```