Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
# SPDX-License-Identifier: AGPL-3.0-or-later

.PHONY: lint test check clean test-config jar
.PHONY: lint test doc check clean test-config jar

test-config:
$(MAKE) -C test-config
Expand All @@ -23,7 +23,11 @@ test: test-config
clean:
rm -rf ./*/classes ./*/target test-config/*pem ./*/*.jar ./*.zip

check: test lint outdated
check: test lint doc outdated
exit $$(git status --porcelain | tee /dev/fd/2 | wc -l) # fail when, after running the above, files in the repo changed

doc:
make -C connector README.md

outdated:
clojure -T:antq outdated
Expand Down
1 change: 1 addition & 0 deletions connector/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

/bdi-connector.jar
/classes/
/interceptors.md
/rules.edn
7 changes: 7 additions & 0 deletions connector/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ bdi-connector.jar: clean
mkdir classes
clojure -M -e "(compile 'org.bdinetwork.connector.main)"
clojure -M:uberjar --main-class org.bdinetwork.connector.main --target $@

interceptors.md: deps.edn src/org/bdinetwork/connector/interceptors.clj src/org/bdinetwork/connector/interceptors/*.clj
clojure -X:print-interceptors > interceptors.md

README.md: interceptors.md README.src.md
echo "<!-- WARNING! THIS FILE IS GENERATED, EDIT README.src.md INSTEAD -->" >$@
sed "/<!-- INCLUDE interceptors.md -->/r $<" README.src.md >>$@
292 changes: 225 additions & 67 deletions connector/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- WARNING! THIS FILE IS GENERATED, EDIT README.src.md INSTEAD -->
<!--
SPDX-FileCopyrightText: 2025 Jomco B.V.
SPDX-FileCopyrightText: 2025 Stichting Connekt
Expand Down Expand Up @@ -102,101 +103,258 @@ An interceptor operates on either the "entering" or "leaving" / "error" phase of

This gateway comes with the following base interceptors:

- `logger` logs incoming requests (method, url and protocol) and response (status and duration) at `info` level in the "leaving" / "error" phase. Note: put this interception in the first position to get proper duration information.
<!-- INCLUDE interceptors.md -->
`[passage.interceptors/logger & [props]]`

Example:
Short name: `logger`

- `GET http://localhost:8081/ HTTP/1.1 / 200 OK / 370ms`
Log incoming requests, response status and duration at `info` level.

Passing an extra expression will add MDC (Mapped Diagnostic Context) to the request log line with evaluated data.
Optional `props` will be evaluated in the "leave" or "error"
phase and logged as diagnostic context, `props` should be a shallow
map with string keys.

Example:
Example log messsage:

- match: `:match {:query-params {"pageNr" page-nr}}`
- interceptor: `[logger {"page-nr" page-nr, "uri" (:uri request)}]`
- request: `http://localhost:8081/test?pageNr=31415`
- log line: `GET http://localhost:8081/ HTTP/1.1 / 200 OK / 370ms page-nr=31415, uri="/test"`
```
GET http://localhost:8081/ HTTP/1.1 / 200 OK / 370ms
```

- `response` produces a literal response in the "entering" phase by evaluation the given expression.
Example with MDC:

Example:
```edn
[logger {"ua" (get-in request [:headers "user-agent"])}]
```

```edn
[response {:status 200, :body "hello world"}]
```
Example log message:

- `request` evaluates an update on the request in the "entering" phase, includes `request` and `ctx` in the evaluation environment.
```
GET http://localhost:8080/ HTTP/1.1 / 200 OK / 123ms ua="curl/1.2.3"
```

Example:
---

```edn
[request assoc-in [:headers "x-request"] "updated"]
```
`[passage.interceptors/proxy url]`

- `response` evaluates an update on the response in the "leaving" phase, includes `request`, `response` and `ctx` in the evaluation environment.
Short name: `proxy`

Example:
Send request to given URL in the "enter" phase and respond.

When it fails to connect to the downstream server, respond with
`503 Service Unavailable`.

Example:

```edn
[proxy (str "https://example.com" (get request :uri))]
```

```edn
[response assoc-in [:headers "x-response"] "updated"]
```
Note: this interceptor should always be the last in the list of
interceptors.

- `proxy` produce a response by executing the (rewritten) request (including the recorded "x-forwarded" headers information in `:proxy-request-overrides`) in the "entering" phase. When the request fails to connect to the down stream server it responds with "503 Service Unavailable".
---

Here are example rules for a minimal reverse proxy to [httpbin](https://httpbin.org):
`[passage.interceptors/request f & args]`

```edn
[
[proxy (str "https://httpbin.org (get request :uri))]
```
Short name: `request`

- `oauth2/bearer-token` require an OAuth 2.0 Bearer token with the given requirements and auth-params for a 401 Unauthorized response. The absence of a token or it not complying to the requirements causes a 401 Unauthorized response. At least the audience `:aud` and issuer `:iss` should be supplied to validate the token. The JWKs are derived from the issuer openid-configuration (issuer is expected to be a URL and the well-known suffix is appended), if not available `jwks-uri` should be supplied. The claims in the token will be available through var `oauth2/claims`.
Update the incoming request in the "enter" phase.

The following example expects a token from example.com and responds with "Hello subject" where "subject" is the "sub" of the token.
Example:

```edn
[oauth2/bearer-token {:iss "http://example.com"
:aud "example"}
{:realm "example"}]
[response assoc :body (str "Hello " (get oauth2/claims :sub))]
[respond {:status 200}]
```
```edn
[request assoc-in [:headers "x-passage"] "passed"]
```

---

`[passage.interceptors/respond response]`

Short name: `respond`

Respond with given value in the "enter" phase.

Example:

```edn
[respond {:status 200
:headers {"content-type" "text/plain"}
:body "hello, world"}]
```

The claims for a valid access token will be place in `ctx` property `:oauth2/bearer-token-claims`.

Both arguments to this intercepted are evaluated as an expression and includes `request` and `ctx` in the evaluation environment.

⚠ Consider removing the "authentication" header from the request after the `oauth2/bearer-token` interceptor using the `request` interceptor, see also [Strip tokens](#strip-tokens). ⚠
Note: this interceptor should always be the last in the list of
interceptors.

- `bdi/authenticate` validate bearer token on incoming request, when none given responds with "401 Unauthorized", otherwise adds "X-Bdi-Client-Id" request header and vars for consumption downstream. Note: put this interceptor *before* `logger` when logging the client-id.
---

Example:
`[passage.interceptors/response f & args]`

```edn
[bdi/authenticate {:server-id "EU.EORI.CONNECTOR"
:private-key #private-key "certs/connector.key.pem"
:public-key #public-key "certs/connector.cert.pem"
:x5c #x5c "certs/connector.x5c.pem"
:association-server-id "EU.EORI.ASSOCIATION-REGISTER"
:association-server-url "https://association-register.com"}]
```
Short name: `response`

- `bdi/deauthenticate` ensure the "X-Bdi-Client-Id" request header is **not** already set on a request for public endpoints which do not need authentication. This prevents clients from fooling the backend into being authenticated. **Always use this on public routes when authentication is optional downstream.**
Update the outgoing response in the "leave" phase.

- `bdi/connect-token` provide a access token endpoint to provide access tokens for machine-to-machine (M2M) operations. Note: this interceptor does no matching, so it needs to be added to a separate rule with a match like: `{:uri "/connect/token", :request-method :post}`.
Example:

```edn
[response update :headers dissoc "server"]
```

---

`[(passage.interceptors.oauth2/set-bearer-token) {:keys [token-endpoint client-id client-secret audience]}]`

Short name: `oauth2/set-bearer-token`

Set a bearer token on the `Authorization` header obtained from the
given `token-endpoint` and credentials.

```edn
[(oauth2/set-bearer-token) {:token-endpoint "http://example.com/token"
:client-id "something"
:client-secret "something secret"
:audience "example"}]
```

---

`[(passage.interceptors.oauth2/validate-bearer-token) {:keys [aud iss], :as requirements} auth-params]`

Short name: `oauth2/validate-bearer-token`

Require and validate OAUTH2 bearer token according to `requirement`.
The absence of a token or it not complying with the requirements
causes a `401 Unauthorized` response including `auth-params`.

At least the audience `:aud` and issuer `:iss` should be supplied to
validate the token. The JWKs are derived from the issuer
openid-configuration (issuer is expected to be a URL and the
well-known suffix is appended); if not available, `:jwks-uri` should
be supplied.

The claims for a valid access token will be placed in the `ctx`
property `:oauth2/bearer-token-claims` and the `Authorization`
header is removed from the `:request` object.

The following example expects a token from "example.com" and
responds with "Hello {subject}" where "{subject}" is the "sub"
of the token.

```edn
[(oauth2/validate-bearer-token) {:iss "http://example.com"
:aud "example"}
{:realm "example"}]
[respond {:status 200
:body (str "Hello " (get-in ctx [:oauth2/bearer-token-claims :sub]))}]
```

---

`[(org.bdinetwork.connector.interceptors/authenticate config)]`

Short name: `bdi/authenticate`

Enforce BDI authentication on incoming requests and add "x-bdi-client-id" request header.
Responds with 401 Unauthorized when request is not allowed. Example:

```
[bdi/authenticate {:server-id "EU.EORI.CONNECTOR"
:private-key #private-key "certs/connector.key.pem"
:public-key #public-key "certs/connector.cert.pem"
:x5c #x5c "certs/connector.x5c.pem"
:association-server-id "EU.EORI.ASSOCIATION-REGISTER"
:association-server-url "https://association-register.com"}]
```

---

`[(org.bdinetwork.connector.interceptors/connect-token config)]`

Short name: `bdi/connect-token`

Provide a token endpoint to provide access tokens for machine-to-machine (M2M) operations.

Note: this interceptor does no matching. Example:

```
{:match {:uri "/connect/token"}
:interceptors
[[bdi/connect-token {:server-id "EU.EORI.CONNECTOR"
:private-key #private-key "certs/connector.key.pem"
:public-key #public-key "certs/connector.cert.pem"
:x5c #x5c "certs/connector.x5c.pem"
:association-server-id "EU.EORI.ASSOCIATION-REGISTER"
:association-server-url "https://association-register.com"}]
..]}
```

---

`[org.bdinetwork.connector.interceptors/deauthenticate]`

Short name: `bdi/deauthenticate`

Ensure the "X-Bdi-Client-Id" request header is **not** already set on a request for public endpoints which do not need authentication.

This prevents clients from fooling the backend into being
authenticated. **Always use this on public routes when
authentication is optional downstream.**

---

`[org.bdinetwork.connector.interceptors/delegation]`

Short name: `bdi/delegation`

Retrieves and evaluates delegation evidence for request.
Responds with 403 Forbidden when the evidence is not found or does
not match the delegation mask.

---

`[(org.bdinetwork.connector.interceptors/demo-audit-log {:keys [json-file], :as opts})]`

Short name: `bdi/demo-audit-log`

Provide access to the last `:n-of-lines` (defaults to 100) lines of `:json-file` (required) and render them in a HTML table.

---

`[org.bdinetwork.connector.interceptors/noodlebar-delegation]`

Short name: `bdi/noodlebar-delegation`

Retrieves and evaluates delegation evidence for request.
Responds with 403 Forbidden when the evidence is not found or does
not match the delegation mask.

---

`[(org.bdinetwork.connector.interceptors/set-bearer-token) {:keys [server-id base-url client-id private-key x5c association-id association-url path]}]`

Short name: `bdi/set-bearer-token`

Set a bearer token on the current request for the given `server-id` and `base-url`.

Example:

```
[(bdi/set-bearer-token) {;; target server
:server-id server-id
:base-url server-url

;; credentials
:client-id server-id
:private-key private-key
:x5c x5c

;; association to check server adherence
:association-url association-server-url
:association-id association-server-id}]
```

Example:
The `:path` can be added for a non-standard token endpoint location,
otherwise `/connect/token` is used.

```edn
{:match {:uri "/connect/token"}
:interceptors
[[bdi/connect-token {:server-id "EU.EORI.CONNECTOR"
:private-key #private-key "certs/connector.key.pem"
:public-key #public-key "certs/connector.cert.pem"
:x5c #x5c "certs/connector.x5c.pem"
:association-server-id "EU.EORI.ASSOCIATION-REGISTER"
:association-server-url "https://association-register.com"}]
```

## Evaluation

Expand Down
Loading