Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4fa30f9
Refactor Platform API configuration and update documentation
Thushani-Jayasekera Jul 20, 2026
7ea5df4
Update Platform API authentication modes and configuration documentation
Thushani-Jayasekera Jul 20, 2026
369a38a
Merge remote-tracking branch 'upstream/main' into fix-configs
Thushani-Jayasekera Jul 20, 2026
b05d761
Update go.work.sum and modify test configuration format in harness_te…
Thushani-Jayasekera Jul 20, 2026
ca3bd73
Enhance Platform API README and configuration documentation
Thushani-Jayasekera Jul 20, 2026
0f954c4
Refactor AI Workspace configuration structure and update documentation
Thushani-Jayasekera Jul 20, 2026
ddd2079
Enhance CI integration and update platform API configuration
Thushani-Jayasekera Jul 20, 2026
c9028d6
Refactor AI Workspace configuration and update related documentation
Thushani-Jayasekera Jul 21, 2026
3988959
Refactor AI Workspace authentication configuration and update documen…
Thushani-Jayasekera Jul 21, 2026
f2e28d7
Refactor logging configuration in AI Workspace and Platform API
Thushani-Jayasekera Jul 21, 2026
8acf2ea
Refactor AI Workspace configuration to utilize koanf for TOML parsing
Thushani-Jayasekera Jul 21, 2026
a05107c
Update authentication configuration to use RS256 asymmetric signing f…
Thushani-Jayasekera Jul 21, 2026
2f4294b
Update authentication configuration in harness_test.go to use IDP mod…
Thushani-Jayasekera Jul 21, 2026
b021650
Add developer portal configuration and update Docker Compose for RS25…
Thushani-Jayasekera Jul 21, 2026
caea33b
Update BFF configuration to support local development with static dir…
Thushani-Jayasekera Jul 22, 2026
a6660d2
Merge remote-tracking branch 'upstream/main' into fix-configs
Thushani-Jayasekera Jul 22, 2026
fffab52
Refactor authMiddleware to enhance token verification logic.
Thushani-Jayasekera Jul 22, 2026
fb27bed
Merge remote-tracking branch 'upstream/main' into fix-configs
Thushani-Jayasekera Jul 22, 2026
9c7cb37
Update go.sum to remove redundant module entry for cloudflare/circl
Thushani-Jayasekera Jul 22, 2026
ef7808e
Update go.mod and go.sum files to add new dependencies and update exi…
Thushani-Jayasekera Jul 22, 2026
b881d85
Refactor JWT configuration to use file paths for public and private k…
Thushani-Jayasekera Jul 22, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ai-workspace-pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
# requests; 502/000 means it is still warming up.
run: |
for i in $(seq 1 30); do
STATUS=$(curl -sk -o /dev/null -w "%{http_code}" https://host.docker.internal:5380/api/proxy/api/v0.9/organizations)
STATUS=$(curl -sk -o /dev/null -w "%{http_code}" https://host.docker.internal:5380/proxy/api/v0.9/organizations)
if [ "$STATUS" = "200" ] || [ "$STATUS" = "401" ] || [ "$STATUS" = "403" ]; then
echo "API proxy ready (HTTP $STATUS)"
exit 0
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/devportal-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build platform-api image
run: make -C platform-api build IMAGE_NAME=platform-api VERSION=it-devportal

- name: Build developer-portal image
run: make -C portals/developer-portal build

- name: Run REST API integration tests (${{ matrix.db }})
env:
PLATFORM_API_IMAGE: platform-api:it-devportal
run: |
if [ "${{ matrix.db }}" = "postgres" ]; then
make -C portals/developer-portal/it test-rest-api-postgres
Expand Down Expand Up @@ -70,10 +75,15 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build platform-api image
run: make -C platform-api build IMAGE_NAME=platform-api VERSION=it-devportal

- name: Build developer-portal image
run: make -C portals/developer-portal build

- name: Run Cypress integration tests
env:
PLATFORM_API_IMAGE: platform-api:it-devportal
run: make -C portals/developer-portal it

- name: Upload Cypress reports
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,5 @@ dev-policies/

gateway/target/

portals/ai-workspace/docker-compose.yaml

# Devportal
portals/developer-portal/target/
48 changes: 48 additions & 0 deletions distribution/all-in-one/devportal-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Developer Portal configuration for the all-in-one (build-from-source) compose
# stack. Mounted at /app/configs/config.toml — replaces the image's shipped
# configs/config.toml, which fails closed at startup on the required security
# secrets and only wires the SQLite `file` key (this stack shares one Postgres
# server with platform-api).
#
# There is NO automatic APIP_DP_* environment-variable override (see
# src/config/configLoader.js) — an env var set in docker-compose.yaml's
# `devportal.environment` block only takes effect where a key below explicitly
# references it via a {{ env "..." }} token.

[server]
base_url = '{{ env "APIP_DP_SERVER_BASEURL" "http://localhost:3001" }}'
port = 3000

[tls]
# Plain HTTP — nothing in this stack talks to devportal over TLS.
enabled = false

[logging]
console_only = true

[database]
type = '{{ env "APIP_DP_DATABASE_TYPE" "postgres" }}'
host = '{{ env "APIP_DP_DATABASE_HOST" "postgres" }}'
port = '{{ env "APIP_DP_DATABASE_PORT" "5432" }}'
name = '{{ env "APIP_DP_DATABASE_NAME" "devportal" }}'
username = '{{ env "APIP_DP_DATABASE_USERNAME" "postgres" }}'
password = '{{ env "APIP_DP_DATABASE_PASSWORD" "postgres" }}'

[organization]
default_name = '{{ env "APIP_DP_ORGANIZATION_DEFAULTNAME" "default" }}'

[platform_api]
# platform-api signs admin JWTs with RS256 — there is no shared HMAC secret to
# set here. jwt_secret left empty makes the devportal decode the token payload
# without verifying its signature, trusting the direct HTTPS connection
# (insecure=true) instead — see extractPlatformJwtClaims in
# src/utils/platformJwt.js. platform-api's cert is self-signed (platform-api-certgen).
base_url = '{{ env "APIP_DP_PLATFORMAPI_BASEURL" "https://platform-api:9243" }}'
insecure = true

[security]
# Required — devportal fails closed at startup if either doesn't resolve to a
# 64-char hex string. Set APIP_DP_SECURITY_ENCRYPTIONKEY / APIP_DP_SECURITY_SESSIONSECRET
# in the environment before `docker compose up` (e.g. `openssl rand -hex 32` each).
encryption_key = '{{ env "APIP_DP_SECURITY_ENCRYPTIONKEY" }}'
session_secret = '{{ env "APIP_DP_SECURITY_SESSIONSECRET" }}'
69 changes: 60 additions & 9 deletions distribution/all-in-one/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,31 @@ services:
dockerfile: Dockerfile
container_name: devportal
environment:
- APIP_DP_PLATFORMAPI_JWTSECRET=${APIP_CP_AUTH_JWT_SECRET_KEY:-}
- APIP_DP_SERVER_BASEURL=http://localhost:3001
- APIP_DP_DATABASE_TYPE=postgres
- APIP_DP_DATABASE_HOST=postgres
- APIP_DP_DATABASE_PORT=5432
- APIP_DP_DATABASE_NAME=devportal
- APIP_DP_DATABASE_USERNAME=postgres
- APIP_DP_DATABASE_PASSWORD=postgres
- APIP_DP_PLATFORMAPI_BASEURL=https://platform-api:9243
# Required — devportal fails closed at startup if either doesn't resolve
# to a 64-char hex string. Set both before `docker compose up`, e.g.:
# export APIP_DP_SECURITY_ENCRYPTIONKEY=$(openssl rand -hex 32)
# export APIP_DP_SECURITY_SESSIONSECRET=$(openssl rand -hex 32)
- APIP_DP_SECURITY_ENCRYPTIONKEY=${APIP_DP_SECURITY_ENCRYPTIONKEY:-}
- APIP_DP_SECURITY_SESSIONSECRET=${APIP_DP_SECURITY_SESSIONSECRET:-}
ports:
- "3001:3001"
- "3001:3000"
volumes:
- ../../portals/developer-portal/config.json:/app/config.json:ro
- ../../portals/developer-portal/secret.json:/app/secret.json:ro
# Replaces the image's shipped configs/config.toml (SQLite-only, fails
# closed on security secrets) — see devportal-config.toml for why.
- ./devportal-config.toml:/app/configs/config.toml:ro
depends_on:
postgres:
condition: service_healthy
platform-api-certgen:
condition: service_completed_successfully
platform-api:
condition: service_healthy

# One-shot init container: generates the TLS pair the platform-api HTTPS
# listener requires (the server no longer generates a self-signed fallback).
Expand All @@ -68,20 +82,50 @@ services:
volumes:
- platform-api-certs:/certs

# One-shot init container: generates the RS256 keypair platform-api's
# auth.jwt config requires (config.toml reads it via {{ file }}). Tokens are
# signed asymmetrically — there is no shared HMAC secret anymore.
platform-api-jwtkeygen:
image: alpine/openssl
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
[ -f /keys/jwt_private.pem ] && [ -f /keys/jwt_public.pem ] && exit 0
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
-out /keys/jwt_private.pem 2>/dev/null
openssl rsa -in /keys/jwt_private.pem -pubout \
-out /keys/jwt_public.pem 2>/dev/null
# jwtkeygen runs as root; platform-api runs as uid 10001, so the private
# key must be owned by that uid, not made world-readable.
chown 10001 /keys/jwt_private.pem
chmod 0600 /keys/jwt_private.pem
chmod 0644 /keys/jwt_public.pem
volumes:
- platform-api-jwt-keys:/keys

platform-api:
image: ghcr.io/wso2/api-platform/platform-api:latest
image: ghcr.io/wso2/api-platform/platform-api:latest
build:
context: ../../platform-api
dockerfile: Dockerfile
additional_contexts:
common: ../../common
# Mounts the shipped default config (../../platform-api/config/config.toml)
# so the APIP_CP_* env vars below actually take effect — {{ env }} / {{ file }}
# tokens only resolve from a loaded config file, they are not read directly.
command: ["-config", "/etc/platform-api/config.toml"]
ports:
- "9243:9243"
volumes:
- ../../platform-api/config/config.toml:/etc/platform-api/config.toml:ro
- platform-api-data:/api-platform/data
- platform-api-certs:/app/data/certs
# RS256 JWT signing/verification keys — on the Platform API's {{ file }}
# allowlist (/etc/platform-api).
- platform-api-jwt-keys:/etc/platform-api/keys:ro
environment:
- APIP_CP_DEFAULT_DEVPORTAL_API_URL=http://devportal:3001
- APIP_CP_DEFAULT_DEVPORTAL_API_URL=http://devportal:3000
- APIP_CP_DATABASE_DRIVER=postgres
- APIP_CP_DATABASE_HOST=postgres
- APIP_CP_DATABASE_PORT=5432
Expand All @@ -93,11 +137,16 @@ services:
- APIP_CP_DATABASE_MAX_IDLE_CONNS=10
- APIP_CP_DATABASE_CONN_MAX_LIFETIME=300
- APIP_CP_DATABASE_EXECUTE_SCHEMA_DDL=true
# Required — set before `docker compose up`, e.g.:
# export APIP_CP_ENCRYPTION_KEY=$(openssl rand -hex 32)
- APIP_CP_ENCRYPTION_KEY=${APIP_CP_ENCRYPTION_KEY:-}
- APIP_CP_AUTH_JWT_SECRET_KEY=${APIP_CP_AUTH_JWT_SECRET_KEY:-}
depends_on:
postgres:
condition: service_healthy
platform-api-certgen:
condition: service_completed_successfully
platform-api-jwtkeygen:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "-O", "/dev/null", "--no-check-certificate", "https://localhost:9243/health"]
interval: 30s
Expand All @@ -120,4 +169,6 @@ volumes:
driver: local
platform-api-certs:
driver: local
platform-api-jwt-keys:
driver: local
postgres_data:
2 changes: 1 addition & 1 deletion docs/ai-workspace/authentication/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Authentication

AI Workspace supports two authentication modes, configured via the `auth_mode` key in `config.toml`.
AI Workspace supports two authentication modes, configured via the `mode` key in `[ai_workspace.auth]` in `config.toml`.

## Modes

Expand Down
57 changes: 29 additions & 28 deletions docs/ai-workspace/authentication/asgardeo-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,19 @@ In each sub-organization that should have access:
Update `configs/config-platform-api.toml`:

```toml
[auth.jwt]
enabled = false
[auth]
mode = "idp"

[auth.idp]
enabled = true
name = "asgardeo"
jwks_url = "https://api.asgardeo.io/t/<your-tenant>/oauth2/jwks"
issuer = ["https://api.asgardeo.io/t/<your-tenant>/oauth2/token"]
audience = ["<ai-workspace-client-id>"]

[auth.idp.claim_mappings]
organization_claim_name = "org_id"
org_name_claim_name = "org_name"
org_handle_claim_name = "org_handle"

[auth.file_based]
enabled = false
[auth.claim_mappings]
organization = "org_id"
org_name = "org_name"
org_handle = "org_handle"
```

> Asgardeo uses `org_id` as the claim for the organization UUID. The Platform API defaults to `organization`, so the claim name override above is required.
Expand All @@ -145,15 +141,20 @@ enabled = false
Update `configs/config.toml`:

```toml
[ai_workspace]
domain = "<your-domain>"
auth_mode = "oidc"
controlplane_host = "<platform-api-host>"
default_org_region = "us"

[platform_api]
[ai_workspace.control_plane]
url = "https://<platform-api-host>"

[oidc]
[ai_workspace.gateway]
controlplane_host = "<platform-api-host>"

[ai_workspace.auth]
mode = "oidc"

[ai_workspace.auth.oidc]
authority = "https://api.asgardeo.io/t/<your-tenant>/oauth2/token"
client_id = "<ai-workspace-client-id>"

Expand All @@ -162,23 +163,23 @@ redirect_url = "https://<your-domain>/api/auth/callback" # the BFF
post_logout_redirect_url = "https://<your-domain>/login"

# Preferred — a mounted secret file. To read it from the git-ignored api-platform.env instead, swap the
# token for '{{ env "APIP_AIW_OIDC_CLIENT_SECRET" }}': the key needs one token or the other.
# token for '{{ env "APIP_AIW_AUTH_OIDC_CLIENT_SECRET" }}': the key needs one token or the other.
client_secret = '{{ file "/secrets/ai-workspace/oidc_client_secret" }}'

# Mirrors [auth.idp.claim_mappings] in config-platform-api.toml — the two must agree.
# Must stay the last table under [oidc]: plain [oidc] keys placed below this header
# would land in [oidc.claim_mappings] instead.
[oidc.claim_mappings]
organization_claim_name = "org_id"
org_name_claim_name = "org_name"
org_handle_claim_name = "org_handle"
# Mirrors [platform_api.auth.claim_mappings] in config-platform-api.toml — the two must
# agree. A sibling of [ai_workspace.auth.oidc], not nested in it: this table applies to
# both auth modes, since basic-mode tokens are signed using these same mapped claim names.
[ai_workspace.auth.claim_mappings]
organization = "org_id"
org_name = "org_name"
org_handle = "org_handle"
```

The redirect URLs and the client secret are BFF settings and never reach the browser. The
redirect URLs are ordinary `config.toml` keys; the secret is referenced with an interpolation
token so the raw value never lands in the file.

> `[oidc] redirect_url` must exactly match the authorized redirect URL registered in section 2.
> `[ai_workspace.auth.oidc] redirect_url` must exactly match the authorized redirect URL registered in section 2.
> A missing client secret fails startup — see [Configuration → Secrets](../configuration.md#secrets).

---
Expand All @@ -188,13 +189,13 @@ token so the raw value never lands in the file.
```
Asgardeo token
├── sub → user identity
├── org_id → organization UUID (→ organization_claim_name in Platform API)
├── org_name → org display name (→ org_name_claim_name in Platform API)
├── org_handle → org slug (→ org_handle_claim_name in Platform API)
├── org_id → organization UUID (→ organization in Platform API)
├── org_name → org display name (→ org_name in Platform API)
├── org_handle → org slug (→ org_handle in Platform API)
└── scope → space-separated ap:* scopes validated by Platform API
```

The claim names must be consistent across all three places:
- Asgardeo token mapper output
- `[oidc.claim_mappings]` in `config.toml`
- `*_claim_name` in Platform API `[auth.idp.claim_mappings]`
- `[ai_workspace.auth.claim_mappings]` in `config.toml`
- The matching keys in Platform API `[auth.claim_mappings]`
32 changes: 17 additions & 15 deletions docs/ai-workspace/authentication/file-based-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,32 @@ File-based auth (also called `basic` mode) stores a user list in the Platform AP

## How It Works

When `auth_mode = "basic"`, the AI Workspace login page renders a username/password form. Credentials are sent to the Platform API, which validates them against a hashed user list in `config-platform-api.toml`. On success, the Platform API issues a signed JWT that the UI stores and sends with subsequent API requests.
When `[ai_workspace.auth] mode = "basic"`, the AI Workspace login page renders a username/password form. Credentials are sent to the Platform API, which validates them against a hashed user list in `config-platform-api.toml`. On success, the Platform API issues a signed JWT that the UI stores and sends with subsequent API requests.

## Configuration

### 1. Set auth mode in `configs/config.toml`

```toml
auth_mode = "basic"
[ai_workspace.auth]
mode = "basic"
```

### 2. Define users in `configs/config-platform-api.toml`

```toml
[auth.file_based]
enabled = true
[auth]
mode = "file"

[[auth.file_based.users]]
username = "admin"
[[auth.file.users]]
username = "admin"
password_hash = "$2a$10$..." # bcrypt hash of the password
role = "admin"
scopes = "ap:organization:manage ap:gateway:manage ..." # space-separated ap:* scopes

[[auth.file_based.users]]
username = "viewer"
[[auth.file.users]]
username = "viewer"
password_hash = "$2a$10$..."
role = "viewer"
scopes = "ap:organization:read ap:gateway:read"
```

### 3. Generate password hashes
Expand Down Expand Up @@ -59,13 +60,14 @@ The Quick Start bundle ships with a default `admin` / `admin` credential — **c
In file-based mode, all users belong to a single organization defined in `config-platform-api.toml`:

```toml
[auth.file_based.organization]
id = "" # Leave empty to auto-generate a UUID on first start
name = "My Organization"
handle = "my-org"
[auth.file.organization]
id = "default" # organization handle (URL-safe slug)
display_name = "My Organization"
region = "us"
uuid = "" # Leave empty to auto-generate a UUID on first start
```

If `id` is left empty, the Platform API generates a stable UUID on first startup and writes it back to the config. This UUID persists across restarts as long as the config file is preserved.
If `uuid` is left empty, the Platform API generates a stable UUID on first startup. Pin it to keep the organization stable across fresh databases.

## Limitations

Expand Down
Loading