Skip to content

Commit bce9634

Browse files
mevan-karuLakshanSS
authored andcommitted
fix: support two different clients for service and user clients
Signed-off-by: Mevan <mevan.karu@gmail.com>
1 parent 3d7cedd commit bce9634

1 file changed

Lines changed: 66 additions & 45 deletions

File tree

docs/platform-engineer-guide/backstage-configuration.mdx

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,41 @@ By default, OpenChoreo configures Thunder as the identity provider for Backstage
4747

4848
### Authentication
4949

50-
Create an OAuth 2.0 client with the following requirements:
50+
Backstage uses **two separate OAuth 2.0 clients**:
5151

52-
**OAuth Client Requirements:**
52+
| Client | Grant type | Purpose |
53+
| ------------------ | -------------------- | ------------------------------------------ |
54+
| **Sign-in client** | `authorization_code` | User login via browser |
55+
| **Service client** | `client_credentials` | Background tasks — catalog sync, API calls |
56+
57+
You can register them as a single OAuth client that supports both grant types, or as two distinct clients. Using two distinct clients is recommended for production as it lets you apply least-privilege scopes to each and rotate them independently.
5358

54-
1. **Grant Types**: The OAuth client must support both:
55-
- `authorization_code` - For user authentication and login flows
56-
- `client_credentials` - For service-to-service authentication
59+
**OAuth Client Requirements:**
5760

58-
2. **Token Format**: Configure the client to issue **JWT tokens** (not opaque tokens)
61+
For the **sign-in client**:
5962

63+
1. **Grant Types**: `authorization_code`
64+
2. **Token Format**: JWT tokens (not opaque tokens)
6065
3. **Redirect URLs**: Add the Backstage callback URL:
6166
- `<protocol>://<backstage-domain>/api/auth/openchoreo-auth/handler/frame`
62-
- Replace `<protocol>` with `http` or `https` and `<backstage-domain>` with your actual Backstage domain
67+
4. **User Claims**: Configure the access token to include:
68+
- `family_name`, `given_name`, `email`, `groups`
69+
70+
For the **service client**:
6371

64-
4. **User Claims**: Configure the access token to include the following claims:
65-
- `family_name` - User's last name
66-
- `given_name` - User's first name
67-
- `email` - User's email address
68-
- `groups` - Groups the user belongs to
72+
1. **Grant Types**: `client_credentials`
73+
2. **Token Format**: JWT tokens
6974

7075
**Helm Configuration:**
7176

72-
Once you have created the OAuth client, create/update a Backstage credentials secret and configure Backstage to use it:
77+
Add both client secrets to the Backstage credentials Secret. If you are using a single shared client, set both `client-secret` and `service-client-secret` to the same value:
7378

7479
```bash
7580
kubectl create secret generic backstage-secrets \
7681
-n openchoreo-control-plane \
7782
--from-literal=backend-secret="your-32-character-secret-here" \
78-
--from-literal=client-secret="your-client-secret" \
83+
--from-literal=client-secret="your-sign-in-client-secret" \
84+
--from-literal=service-client-secret="your-service-client-secret" \
7985
--from-literal=jenkins-api-key="not-used" \
8086
--dry-run=client -o yaml | kubectl apply -f -
8187
```
@@ -84,27 +90,35 @@ kubectl create secret generic backstage-secrets \
8490
backstage:
8591
secretName: "backstage-secrets"
8692
auth:
87-
clientId: "your-client-id"
93+
# Sign-in client (authorization_code flow)
94+
clientId: "your-sign-in-client-id"
8895
redirectUrls:
8996
- "<protocol>://<backstage-domain>/api/auth/openchoreo-auth/handler/frame"
9097
oidcScope: "openid profile email"
98+
# Service client (client_credentials flow — background tasks)
99+
serviceClientId: "your-service-client-id"
100+
serviceClientSecretKey: "service-client-secret"
91101
scope: ""
92102
```
93103

104+
:::note
105+
If `serviceClientId` is not set, it falls back to `clientId`. If `serviceClientSecretKey` is not set, it falls back to `client-secret`. This means existing single-client deployments require no changes.
106+
:::
107+
94108
**Scope Configuration:**
95109

96-
| Field | Description | Default |
97-
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
98-
| `backstage.auth.oidcScope` | Space-separated scopes requested during user login via the `authorization_code` flow | `"openid profile email"` |
99-
| `backstage.auth.scope` | Space-separated scopes requested when the Backstage backend requests service tokens via `client_credentials`. Leave empty to use the IdP default, or set explicitly when required by your IdP (e.g. `"api://client-id/.default"` for Azure AD). | `""` |
110+
| Field | Description | Default |
111+
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
112+
| `backstage.auth.oidcScope` | Space-separated scopes requested during user login via the `authorization_code` flow (sign-in client) | `"openid profile email"` |
113+
| `backstage.auth.scope` | Space-separated scopes requested when the service client fetches tokens via `client_credentials`. Leave empty to use the IdP default, or set explicitly when required by your IdP (e.g. `"api://client-id/.default"` for Azure AD). | `""` |
100114

101115
See [Identity Provider Configuration](./identity-configuration.mdx) for detailed setup instructions.
102116

103117
### Authorization
104118

105-
With authorization enabled by default, Backstage uses the `client_credentials` grant to authenticate with the OpenChoreo API as a service account. The API matches the `sub` claim in the issued JWT to identify the caller, so the new client must be granted the `backstage-catalog-reader` role via a bootstrap authorization mapping.
119+
With authorization enabled by default, Backstage uses the `client_credentials` grant (via the service client) to authenticate with the OpenChoreo API as a service account. The API matches the `sub` claim in the issued JWT to identify the caller, so the service client must be granted the `backstage-catalog-reader` role via a bootstrap authorization mapping.
106120

107-
Add the following to your values override file, replacing `your-client-id` with the same client ID used in the authentication configuration above:
121+
Add the following to your values override file, replacing `your-service-client-id` with the value of `backstage.auth.serviceClientId` (or `backstage.auth.clientId` if you are using a single shared client):
108122

109123
```yaml
110124
openchoreoApi:
@@ -122,7 +136,7 @@ openchoreoApi:
122136
kind: ClusterAuthzRole
123137
entitlement:
124138
claim: sub
125-
value: "your-client-id"
139+
value: "your-service-client-id"
126140
effect: allow
127141
```
128142

@@ -216,28 +230,31 @@ backstage:
216230

217231
The chart sets the following environment variables on the Backstage container. Variables marked "from Secret" are read from the Secret referenced by `backstage.secretName`.
218232

219-
| Variable | Description | Source |
220-
| ------------------------------------------------ | --------------------------------------------- | ------------------------------------------------------------ |
221-
| `NODE_ENV` | Node.js environment | `backstage.env` (default: `production`) |
222-
| `LOG_LEVEL` | Logging verbosity | `backstage.env` (default: `info`) |
223-
| `PORT` | HTTP server port | `backstage.env` (default: `7007`) |
224-
| `BACKSTAGE_BASE_URL` | Public URL for OAuth redirects and links | `backstage.baseUrl` |
225-
| `OPENCHOREO_API_URL` | Internal OpenChoreo API endpoint | `backstage.openchoreoApi.url` (auto-configured) |
226-
| `BACKEND_SECRET` | Session encryption key | from Secret (`backend-secret`) |
227-
| `OPENCHOREO_AUTH_CLIENT_ID` | OAuth client ID | `backstage.auth.clientId` |
228-
| `OPENCHOREO_AUTH_CLIENT_SECRET` | OAuth client secret | from Secret (`client-secret`) |
229-
| `OPENCHOREO_AUTH_AUTHORIZATION_URL` | OAuth authorization endpoint | `security.oidc.authorizationUrl` |
230-
| `OPENCHOREO_AUTH_TOKEN_URL` | OAuth token endpoint | `security.oidc.tokenUrl` |
231-
| `OPENCHOREO_AUTH_OIDC_SCOPE` | OIDC scopes | `backstage.auth.oidcScope` (default: `openid profile email`) |
232-
| `OPENCHOREO_FEATURES_AUTH_ENABLED` | Enable authentication | `security.enabled` |
233-
| `OPENCHOREO_FEATURES_AUTHZ_ENABLED` | Enable authorization | `security.authz.enabled` |
234-
| `OPENCHOREO_FEATURES_AUTH_REDIRECT_FLOW_ENABLED` | Silent redirect vs sign-in popup | `backstage.features.auth.redirectFlow.enabled` |
235-
| `OPENCHOREO_FEATURES_WORKFLOWS_ENABLED` | Show Workflows UI | `backstage.features.workflows.enabled` |
236-
| `OPENCHOREO_FEATURES_OBSERVABILITY_ENABLED` | Show Metrics/Traces/Logs UI | `backstage.features.observability.enabled` |
237-
| `OPENCHOREO_EVENTS_ENABLED` | Enable event-driven catalog sync | `backstage.events.enabled` |
238-
| `OPENCHOREO_CATALOG_SYNC_FREQUENCY` | Periodic full-sync interval (seconds) | `backstage.catalogSync.frequency` |
239-
| `DATABASE_CLIENT` | Database driver (`better-sqlite3` or `pg`) | `backstage.database.type` |
240-
| `SQLITE_STORAGE_DIR` | SQLite database directory (when using SQLite) | `backstage.database.sqlite.mountPath` |
233+
| Variable | Description | Source |
234+
| ------------------------------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
235+
| `NODE_ENV` | Node.js environment | `backstage.env` (default: `production`) |
236+
| `LOG_LEVEL` | Logging verbosity | `backstage.env` (default: `info`) |
237+
| `PORT` | HTTP server port | `backstage.env` (default: `7007`) |
238+
| `BACKSTAGE_BASE_URL` | Public URL for OAuth redirects and links | `backstage.baseUrl` |
239+
| `OPENCHOREO_API_URL` | Internal OpenChoreo API endpoint | `backstage.openchoreoApi.url` (auto-configured) |
240+
| `BACKEND_SECRET` | Session encryption key | from Secret (`backend-secret`) |
241+
| `OPENCHOREO_AUTH_CLIENT_ID` | Sign-in client ID (authorization_code flow) | `backstage.auth.clientId` |
242+
| `OPENCHOREO_AUTH_CLIENT_SECRET` | Sign-in client secret | from Secret (`client-secret`) |
243+
| `OPENCHOREO_SERVICE_CLIENT_ID` | Service client ID (client_credentials flow — background tasks) | `backstage.auth.serviceClientId` (falls back to `clientId`) |
244+
| `OPENCHOREO_SERVICE_CLIENT_SECRET` | Service client secret | from Secret (key `backstage.auth.serviceClientSecretKey`, default: `client-secret`) |
245+
| `OPENCHOREO_AUTH_AUTHORIZATION_URL` | OAuth authorization endpoint | `security.oidc.authorizationUrl` |
246+
| `OPENCHOREO_AUTH_TOKEN_URL` | OAuth token endpoint (shared by both clients) | `security.oidc.tokenUrl` |
247+
| `OPENCHOREO_AUTH_OIDC_SCOPE` | OIDC scopes for the sign-in client | `backstage.auth.oidcScope` (default: `openid profile email`) |
248+
| `OPENCHOREO_AUTH_SCOPE` | Scopes for the service client's client_credentials token request | `backstage.auth.scope` (default: `""`) |
249+
| `OPENCHOREO_FEATURES_AUTH_ENABLED` | Enable authentication | `security.enabled` |
250+
| `OPENCHOREO_FEATURES_AUTHZ_ENABLED` | Enable authorization | `security.authz.enabled` |
251+
| `OPENCHOREO_FEATURES_AUTH_REDIRECT_FLOW_ENABLED` | Silent redirect vs sign-in popup | `backstage.features.auth.redirectFlow.enabled` |
252+
| `OPENCHOREO_FEATURES_WORKFLOWS_ENABLED` | Show Workflows UI | `backstage.features.workflows.enabled` |
253+
| `OPENCHOREO_FEATURES_OBSERVABILITY_ENABLED` | Show Metrics/Traces/Logs UI | `backstage.features.observability.enabled` |
254+
| `OPENCHOREO_EVENTS_ENABLED` | Enable event-driven catalog sync | `backstage.events.enabled` |
255+
| `OPENCHOREO_CATALOG_SYNC_FREQUENCY` | Periodic full-sync interval (seconds) | `backstage.catalogSync.frequency` |
256+
| `DATABASE_CLIENT` | Database driver (`better-sqlite3` or `pg`) | `backstage.database.type` |
257+
| `SQLITE_STORAGE_DIR` | SQLite database directory (when using SQLite) | `backstage.database.sqlite.mountPath` |
241258

242259
## HTTP Routing Configuration
243260

@@ -281,9 +298,13 @@ Backstage credentials are loaded from a Kubernetes Secret referenced by `backsta
281298
Required keys:
282299

283300
- `backend-secret`: Backstage session encryption key
284-
- `client-secret`: OAuth client secret
301+
- `client-secret`: Sign-in client secret (authorization_code flow)
285302
- `jenkins-api-key`: Jenkins integration API key (use a placeholder if not using Jenkins)
286303

304+
Optional keys:
305+
306+
- `service-client-secret`: Service client secret (client_credentials flow). Required only when using a dedicated service client (`backstage.auth.serviceClientSecretKey: "service-client-secret"`). When not present, the service client falls back to `client-secret`.
307+
287308
When `backstage.database.type=postgresql`, the same Secret must also include:
288309

289310
- `postgres-host`

0 commit comments

Comments
 (0)