Skip to content

Commit

Permalink
Document OAuth2 client credentials flow for HTTP connections.
Browse files Browse the repository at this point in the history
Signed-off-by: Yufei Cai <yufei.cai@bosch.io>
  • Loading branch information
yufei-cai committed Nov 4, 2021
1 parent a391b0d commit de98475
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 1 deletion.
4 changes: 4 additions & 0 deletions connectivity/service/src/main/resources/connectivity.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Expand Down
211 changes: 211 additions & 0 deletions documentation/src/main/resources/_posts/2021-11-03-oauth2.md
Original file line number Diff line number Diff line change
@@ -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 `<headers>.<body>.<signature>`, where `<headers>` and `<body>` are base64-encoding
of the headers and the body in JSON format, and `<signature>` 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": "<namespace>/<name>/things/twin/events/created",
"headers": {},
"path": "/",
"value": {
"policyId": "<policy-id>"
},
"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.
<br/>
<br/>
{% include image.html file="ditto.svg" alt="Ditto" max-width=500 %}
--<br/>
The Eclipse Ditto team
45 changes: 45 additions & 0 deletions documentation/src/main/resources/jsonschema/connection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<localhost certificate>\n-----END CERTIFICATE-----",
"credentials": {
Expand Down Expand Up @@ -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}
}
}
}
}
}
```

0 comments on commit de98475

Please sign in to comment.