Skip to content

Commit fff1aaa

Browse files
authored
add keycloak client for external API token issuance (#32)
* add keycloak client for external API token issuance Configure a second Keycloak client (runway-api) for the client credentials flow so the external API can be tested locally without an external IdP. Adds create:jobs and partner:ea client scopes, an audience mapper, and updates .env.copyme with working defaults. Update docs with local development curl examples and fix the generic token request example to use form-encoded parameters. Made-with: Cursor * fix link in external api readme
1 parent 845b6e8 commit fff1aaa

4 files changed

Lines changed: 69 additions & 8 deletions

File tree

app/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ There are two methods to log in as a given user:
9393
1. Within the user record, select `Impersonate` from the Action menu in the upper right
9494
1. Go to http://localhost:4200 and click the login button. When you get redirected to Keycloak, Keycloak will authenticate you as the user you just impersonated.
9595

96-
#### 4. Configure an ODS
96+
#### 4. Testing the External API
97+
98+
The local Keycloak comes with a pre-configured client for the external API (client credentials flow). See the [External API README](api/src/external-api/README.md#local-development) for curl examples.
99+
100+
#### 5. Configure an ODS
97101

98102
You'll need a valid (even if non-prod) place to send data in order to run local jobs.
99103

app/api/.env.copyme

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ LOCAL_EVENT_EMITTER=log # log | noop (omit for EventBridge)
2727
AWS_REGION=us-east-2
2828
BUNDLE_BRANCH=development
2929

30-
# OAUTH2_AUDIENCE=runway-local
31-
# OAUTH2_ISSUER=<url of token issuer, if using>
30+
OAUTH2_ISSUER=http://localhost:8080/realms/example
31+
OAUTH2_AUDIENCE=runway-local

app/api/keycloak/config.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ realm: example
44
displayName: Example
55
ssoSessionMaxLifespan: 7776000
66
enabled: true
7+
8+
clientScopes:
9+
- name: "create:jobs"
10+
protocol: openid-connect
11+
attributes:
12+
"include.in.token.scope": "true"
13+
- name: "partner:ea"
14+
protocol: openid-connect
15+
attributes:
16+
"include.in.token.scope": "true"
17+
718
clients:
19+
# UI login (authorization code flow)
820
- clientId: runway-local
921
name: 'Runway Local Dev'
1022
enabled: true
@@ -50,6 +62,31 @@ clients:
5062
jsonType.label: String
5163
attributes:
5264
post.logout.redirect.uris: http://localhost:4200*
65+
66+
# External API (client credentials flow)
67+
- clientId: runway-api
68+
name: 'Runway External API'
69+
enabled: true
70+
clientAuthenticatorType: client-secret
71+
secret: api-secret-123
72+
serviceAccountsEnabled: true
73+
standardFlowEnabled: false
74+
directAccessGrantsEnabled: false
75+
defaultClientScopes:
76+
- "create:jobs"
77+
- "partner:ea"
78+
protocolMappers:
79+
# The app validates the aud claim, so we need to inject it.
80+
# Value must match OAUTH2_AUDIENCE.
81+
- name: API Audience
82+
protocol: openid-connect
83+
protocolMapper: oidc-audience-mapper
84+
consentRequired: false
85+
config:
86+
included.custom.audience: "runway-local"
87+
id.token.claim: "false"
88+
access.token.claim: "true"
89+
5390
users:
5491
- username: dev
5592
credentials:

app/api/src/external-api/README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,34 @@ Configure an OAuth2/OIDC client in your identity provider with:
2525

2626
The access token must include the following claims:
2727

28-
| Claim | Required | Description |
29-
| ------------- | -------- | --------------------------------------------------------------------------------------------------- |
30-
| `client_id` | Yes\* | The OAuth2 client ID. Used to attribute jobs to the API client. |
31-
| `azp` | Yes\* | Authorized party. Used as fallback if `client_id` is not present. |
32-
| `client_name` | No | Display name for the API client. If provided, shown in the Runway UI for jobs created via the API. |
28+
| Claim | Required | Description |
29+
| ------------- | -------- | -------------------------------------------------------------------------------------------------- |
30+
| `client_id` | Yes\* | The OAuth2 client ID. Used to attribute jobs to the API client. |
31+
| `azp` | Yes\* | Authorized party. Used as fallback if `client_id` is not present. |
32+
| `client_name` | No | Display name for the API client. If provided, shown in the Runway UI for jobs created via the API. |
3333

3434
\* At least one of `client_id` or `azp` must be present. Most OAuth2 providers include `client_id` by default in client credentials tokens.
3535

36+
### Local Development
37+
38+
The local Keycloak instance (started by `docker compose`) comes pre-configured with a `runway-api` client for the external API. No additional IdP setup is needed.
39+
40+
The client is configured in [`api/keycloak/config.yaml`](../../../api/keycloak/config.yaml) with the `create:jobs` and `partner:ea` scopes and an audience of `runway-local`.
41+
42+
Get a token and verify it:
43+
44+
```bash
45+
# Obtain a token from local Keycloak
46+
TOKEN=$(curl -s -X POST http://localhost:8080/realms/example/protocol/openid-connect/token \
47+
-d "grant_type=client_credentials" \
48+
-d "client_id=runway-api" \
49+
-d "client_secret=api-secret-123" | jq -r '.access_token')
50+
51+
# Verify it against the local API
52+
curl -X POST http://localhost:3333/api/v1/token/verify \
53+
-H "Authorization: Bearer $TOKEN"
54+
```
55+
3656
## API Usage
3757

3858
All requests must include:

0 commit comments

Comments
 (0)