From 8650a8ec19689c466d09917d162b22ffe4430fc5 Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Wed, 17 Jun 2026 15:30:18 +0200 Subject: [PATCH 1/2] feat: add token script --- README.md | 30 ++++++++++-------- scripts/token.sh | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 12 deletions(-) create mode 100755 scripts/token.sh diff --git a/README.md b/README.md index 652a59d..6dbf4d8 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,8 @@ traefik LoadBalancer 10.96.251.221 172.18.0.3 80:31415/TCP,443:31650/TC #### 2.1 Option 1: Use pre-built images -There are pre-built images for all JAD apps available from [GHCR](https://github.com/eclipse-dataspace-hub/jad/packages) and the +There are pre-built images for all JAD apps available from [GHCR](https://github.com/eclipse-dataspace-hub/jad/packages) +and the Connector Fabric Manager images are available from the [CFM GitHub Repository](https://github.com/eclipse-cfm/cfm/packages). Those are tested and we strongly recommend using them. @@ -306,6 +307,11 @@ be put in place by running the REST requests in the `CFM - Provision Consumer` f in the [Bruno collection](./requests/EDC-V%20Onboarding). Be sure to select the `"KinD Local"` environment in Bruno. +Since we switched to token exchange for authentication, the requests in the Bruno collection have been updated to use +the `Authorization` with a fixed Bearer token. A token can be obtained by creating a service account token with +`kubectl` and running the token exchange flow. The script `scripts/token.sh` does this +automatically and print the access token. Run it and copy the token into the Bruno environment at collection level. + ![bruno.png](docs/images/bruno.png) Those requests can be run manually, one after the other, or via Bruno's "Run" feature. It may be necessary to manually @@ -425,20 +431,20 @@ unauthenticated at the gateway level. ### Application routes (`jad.localhost`) -| Service | Exposed path | Rewrites to | Backend port | Auth middleware | -|---------------------|---------------------|-------------------------|--------------|----------------------------------------| -| Control Plane | `/api/management` | `/api/mgmt` | `8081` | `jwt-auth-management-api` | +| Service | Exposed path | Rewrites to | Backend port | Auth middleware | +|---------------------|---------------------|------------------------|--------------|----------------------------------------| +| Control Plane | `/api/management` | `/api/mgmt` | `8081` | `jwt-auth-management-api` | | Identity Hub | `/api/identity` | `/api/identity/v1beta` | `7081` | `jwt-auth-identity-api` | | Issuer Service | `/api/issuer/admin` | `/api/admin/v1beta` | `10013` | `jwt-auth-issuer-admin-api` | | Provision Manager | `/api/pm` | `/api/v1beta` | `8080` | `jwt-auth-provision-manager-api` | -| Tenant Manager | `/api/tm` | `/api/v1alpha1` | `8080` | `jwt-auth-tenant-manager-api` | -| Dataplane (public) | `/api/dp/public` | `/` | `11002` | — | -| Dataplane (control) | `/api/dp/control` | `/` | `8083` | — | -| Dataplane (certs) | `/api/dp/certs` | `/` | `8186` | — | -| Siglet | `/api/siglet` | `/` | `8080` | — | -| Redline | `/redline` | `/` | `8081` | — | -| Keycloak | `/auth` | `/` | `8080` | — (is the auth server) | -| Web UI | `/ui` | `/` | `80` | — (obtains its own token via Keycloak) | +| Tenant Manager | `/api/tm` | `/api/v1alpha1` | `8080` | `jwt-auth-tenant-manager-api` | +| Dataplane (public) | `/api/dp/public` | `/` | `11002` | — | +| Dataplane (control) | `/api/dp/control` | `/` | `8083` | — | +| Dataplane (certs) | `/api/dp/certs` | `/` | `8186` | — | +| Siglet | `/api/siglet` | `/` | `8080` | — | +| Redline | `/redline` | `/` | `8081` | — | +| Keycloak | `/auth` | `/` | `8080` | — (is the auth server) | +| Web UI | `/ui` | `/` | `80` | — (obtains its own token via Keycloak) | ### Auth middleware scopes diff --git a/scripts/token.sh b/scripts/token.sh new file mode 100755 index 0000000..98d3ad9 --- /dev/null +++ b/scripts/token.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2026 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# +# + +# +# Mint a Kubernetes SA token, exchange it (RFC 8693) at the IDP, print the +# resulting access token to stdout. +# +# Usage: +# KUBE_SA=my-sa KUBE_NS=my-ns KUBE_AUDIENCE= \ +# IDP_TOKEN_ENDPOINT=https://idp.example.com/oauth2/token \ +# IDP_AUDIENCE= \ +# ./get-access-token.sh +# +# # capture it: +# TOKEN="$(./get-access-token.sh)" +# curl -H "Authorization: Bearer $TOKEN" https://your-ingress/... +# +set -euo pipefail + +# ---- config (override via env) ------------------------------------------- +: "${KUBE_SA:=redline}" +: "${KUBE_NS:=edc-v}" +: "${KUBE_AUDIENCE:=https://kubernetes.default.svc.cluster.local}" # audience the IDP expects on the subject token +: "${KUBE_TOKEN_DURATION:=1800s}" + +: "${IDP_TOKEN_ENDPOINT:=http://jad.localhost/api/auth/token}" +: "${IDP_AUDIENCE:=edcv}" # audience the ingress expects +: "${IDP_SCOPE:=admin cfm-write cfm-read read write}" + +# ---- 1. mint the SA subject token ---------------------------------------- +kubectl_args=(create token "$KUBE_SA" -n "$KUBE_NS" --duration "$KUBE_TOKEN_DURATION") +if [ -n "$KUBE_AUDIENCE" ]; then kubectl_args+=(--audience "$KUBE_AUDIENCE"); fi + +SUBJECT_TOKEN="$(kubectl "${kubectl_args[@]}")" + +# ---- 2. RFC 8693 token exchange ------------------------------------------ +curl_args=( + --silent --show-error --fail-with-body + --request POST "$IDP_TOKEN_ENDPOINT" + --header "Content-Type: application/x-www-form-urlencoded" + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" + --data-urlencode "subject_token=${SUBJECT_TOKEN}" + --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt" + --data-urlencode "resource=redline" + --data-urlencode "audience=${IDP_AUDIENCE}" +) +if [ -n "$IDP_SCOPE" ]; then curl_args+=(--data-urlencode "scope=${IDP_SCOPE}"); fi + +RESPONSE="$(curl "${curl_args[@]}")" + +# ---- 3. extract + print access_token ------------------------------------- +if command -v jq >/dev/null 2>&1; then + ACCESS_TOKEN="$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty')" +else + # No jq: a JWT value contains only URL-safe base64 chars, so this is safe. + ACCESS_TOKEN="$(printf '%s' "$RESPONSE" \ + | grep -o '"access_token"[[:space:]]*:[[:space:]]*"[^"]*"' \ + | head -n1 \ + | sed 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" +fi + +if [ -z "$ACCESS_TOKEN" ]; then + echo "error: no access_token in IDP response:" >&2 + printf '%s\n' "$RESPONSE" >&2 + exit 1 +fi + +printf '%s\n' "$ACCESS_TOKEN" \ No newline at end of file From 5d84c5ebb181074c8fcbb7523c1eb2c0d1f42aeb Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Wed, 17 Jun 2026 15:47:56 +0200 Subject: [PATCH 2/2] pr suggestions --- scripts/token.sh | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/scripts/token.sh b/scripts/token.sh index 98d3ad9..c7b16bf 100755 --- a/scripts/token.sh +++ b/scripts/token.sh @@ -55,21 +55,13 @@ curl_args=( --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt" --data-urlencode "resource=redline" --data-urlencode "audience=${IDP_AUDIENCE}" + --data-urlencode "scope=${IDP_SCOPE}" ) -if [ -n "$IDP_SCOPE" ]; then curl_args+=(--data-urlencode "scope=${IDP_SCOPE}"); fi RESPONSE="$(curl "${curl_args[@]}")" # ---- 3. extract + print access_token ------------------------------------- -if command -v jq >/dev/null 2>&1; then - ACCESS_TOKEN="$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty')" -else - # No jq: a JWT value contains only URL-safe base64 chars, so this is safe. - ACCESS_TOKEN="$(printf '%s' "$RESPONSE" \ - | grep -o '"access_token"[[:space:]]*:[[:space:]]*"[^"]*"' \ - | head -n1 \ - | sed 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" -fi +ACCESS_TOKEN="$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty')" if [ -z "$ACCESS_TOKEN" ]; then echo "error: no access_token in IDP response:" >&2