Skip to content

Commit 9459968

Browse files
committed
feat: add external IDP integration (hybrid mode)
Signed-off-by: Vladimir Belousov <vbelouso@redhat.com>
1 parent 86eaa51 commit 9459968

File tree

5 files changed

+249
-33
lines changed

5 files changed

+249
-33
lines changed

docs/authentication.md

Lines changed: 161 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ ExploitIQ supports multiple authentication modes via Quarkus profiles:
1212
| `external-idp` | External identity providers | Keycloak, Google, Azure AD, Okta |
1313
| `dev` | Local development | Keycloak DevServices |
1414

15+
### Authentication Methods
16+
17+
All profiles support both browser and API authentication:
18+
19+
| Method | Use Case | Flow |
20+
|--------|----------|------|
21+
| Browser | Web UI | Authorization Code Flow (redirects to IdP) |
22+
| API | CLI, scripts, services | Bearer JWT token in `Authorization` header |
23+
24+
**Token acquisition differs by profile:**
25+
26+
- `prod` (OpenShift): Use `oc whoami -t` or ServiceAccount tokens
27+
- `external-idp` (Keycloak): Use OIDC token endpoint with password grant
28+
- `dev`: Same as `external-idp` (DevServices Keycloak)
29+
1530
## OpenShift OAuth (Production)
1631

1732
The default production configuration uses OpenShift's built-in OAuth server.
@@ -57,6 +72,16 @@ spec:
5772
key: secret
5873
```
5974

75+
### API Access (prod profile)
76+
77+
For API access in OpenShift, use your user token:
78+
79+
```bash
80+
# After oc login
81+
TOKEN=$(oc whoami -t)
82+
curl -H "Authorization: Bearer $TOKEN" https://exploit-iq-client.apps.example.com/api/v1/reports
83+
```
84+
6085
## External Identity Providers
6186

6287
Use the `external-idp` profile to integrate with external OIDC providers.
@@ -84,13 +109,17 @@ Create an OIDC client in Keycloak with the following settings:
84109
"clientId": "exploit-iq-client",
85110
"enabled": true,
86111
"clientAuthenticatorType": "client-secret",
112+
"secret": "<your-client-secret>",
87113
"redirectUris": ["https://your-app-url/*"],
88114
"webOrigins": ["https://your-app-url"],
89115
"publicClient": false,
90-
"standardFlowEnabled": true
116+
"standardFlowEnabled": true,
117+
"directAccessGrantsEnabled": true
91118
}
92119
```
93120

121+
**Important:** `directAccessGrantsEnabled: true` is required for API authentication via password grant.
122+
94123
Required protocol mappers (add to client scope):
95124

96125
- `preferred_username`: Maps `username` to `preferred_username` claim
@@ -149,6 +178,80 @@ The same approach works with any OIDC-compliant provider:
149178

150179
**Note:** GitHub does not support OIDC. Use Keycloak as an identity broker for GitHub authentication.
151180

181+
## API Authentication with JWT (external-idp)
182+
183+
When using Keycloak or other OIDC providers, you can obtain tokens via the standard OIDC token endpoint. This allows CLI tools, scripts, and external services to authenticate without browser interaction.
184+
185+
### Obtaining a User Token
186+
187+
Use the password grant to obtain a token for a specific user:
188+
189+
```bash
190+
# Configuration (match your Keycloak setup)
191+
KC_URL="http://localhost:8190" # Keycloak URL
192+
KC_REALM="quarkus" # Realm name
193+
CLIENT_ID="exploit-iq-client" # Client ID
194+
CLIENT_SECRET="example-credentials" # Client secret
195+
USERNAME="bruce" # User
196+
PASSWORD="wayne" # Password
197+
198+
# Get user token (scope=openid is REQUIRED)
199+
USER_TOKEN=$(curl -s -X POST \
200+
"${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \
201+
-d "client_id=${CLIENT_ID}" \
202+
-d "client_secret=${CLIENT_SECRET}" \
203+
-d "username=${USERNAME}" \
204+
-d "password=${PASSWORD}" \
205+
-d "grant_type=password" \
206+
-d "scope=openid profile email" | jq -r '.access_token')
207+
208+
# Verify token was obtained
209+
echo "Token: ${USER_TOKEN:0:50}..."
210+
```
211+
212+
**Important:** The `scope=openid profile email` parameter is required. Without `openid`, the UserInfo endpoint will reject the token with "Missing openid scope" error.
213+
214+
### Making API Requests
215+
216+
Use the token in the `Authorization` header:
217+
218+
```bash
219+
# List reports
220+
curl -H "Authorization: Bearer $USER_TOKEN" \
221+
http://localhost:8080/api/v1/reports
222+
223+
# Get specific report
224+
curl -H "Authorization: Bearer $USER_TOKEN" \
225+
http://localhost:8080/api/v1/reports/{id}
226+
```
227+
228+
### Service-to-Service Authentication (Optional)
229+
230+
For machine-to-machine communication, use the client credentials grant:
231+
232+
```bash
233+
# Get service token
234+
SERVICE_TOKEN=$(curl -s -X POST \
235+
"${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \
236+
-d "client_id=${SERVICE_CLIENT_ID}" \
237+
-d "client_secret=${SERVICE_SECRET}" \
238+
-d "grant_type=client_credentials" | jq -r '.access_token')
239+
240+
curl -H "Authorization: Bearer $SERVICE_TOKEN" \
241+
http://localhost:8080/api/v1/reports
242+
```
243+
244+
**Note:** Requires a separate Keycloak client configured for service accounts.
245+
246+
### Token Validation
247+
248+
The application validates JWT tokens by:
249+
250+
1. Verifying the signature using JWKS from the IdP
251+
2. Checking token expiration
252+
3. Validating the issuer (`iss` claim)
253+
4. Fetching UserInfo to extract user details
254+
152255
## Identity Brokering with Keycloak
153256

154257
Keycloak can act as an identity broker, allowing users to authenticate via external providers while maintaining centralized user management.
@@ -207,6 +310,8 @@ podman run -d --name keycloak \
207310
-p 8190:8080 \
208311
-e KEYCLOAK_ADMIN=admin \
209312
-e KEYCLOAK_ADMIN_PASSWORD=admin \
313+
-e KC_HTTP_ENABLED=true \
314+
-e KC_HOSTNAME=localhost \
210315
quay.io/keycloak/keycloak:26.4 start-dev
211316

212317
# Start application (using 'quarkus' as example realm name)
@@ -225,14 +330,30 @@ An automated testing script is available for all authentication scenarios:
225330
./scripts/test-auth.sh --help
226331
```
227332

228-
The script supports:
333+
The script supports DevServices Keycloak, external Keycloak (with optional GitHub/Google brokers), and direct Google OIDC.
229334

230-
1. DevServices Keycloak (local development)
231-
2. DevServices + GitHub Broker
232-
3. External Keycloak (standalone)
233-
4. External Keycloak + GitHub Broker
234-
5. External Keycloak + Google Broker
235-
6. Direct Google OIDC
335+
#### Testing API Authentication
336+
337+
After running a scenario with Keycloak, test API authentication:
338+
339+
```bash
340+
# 1. Get user token (uses bruce/wayne created by the script)
341+
USER_TOKEN=$(curl -s -X POST \
342+
"http://localhost:8190/realms/quarkus/protocol/openid-connect/token" \
343+
-d "client_id=exploit-iq-client" \
344+
-d "client_secret=example-credentials" \
345+
-d "username=bruce" \
346+
-d "password=wayne" \
347+
-d "grant_type=password" \
348+
-d "scope=openid profile email" | jq -r '.access_token')
349+
350+
# 2. Verify token obtained
351+
[ -n "$USER_TOKEN" ] && echo "Token obtained" || echo "Failed to get token"
352+
353+
# 3. Call API with Bearer token
354+
curl -H "Authorization: Bearer $USER_TOKEN" \
355+
http://localhost:8080/api/v1/reports
356+
```
236357

237358
## User Display
238359

@@ -262,6 +383,31 @@ Ensure your identity provider or Keycloak is configured to include the `email` c
262383
- Ensure exact match including trailing slash: `https://your-app/`
263384
- Changes may take 5-15 minutes to propagate
264385

386+
### API Returns 401 Unauthorized
387+
388+
**Cause:** Token missing `openid` scope or invalid token.
389+
390+
**Solution:**
391+
392+
1. Ensure `scope=openid profile email` is included in token request
393+
2. Verify token is not expired
394+
3. Check Keycloak logs for "Missing openid scope" error
395+
396+
### HTTPS Required Error (Keycloak)
397+
398+
**Cause:** Keycloak 26.x requires HTTPS by default, even in development.
399+
400+
**Solution:** For local development, set `sslRequired=NONE` on the realm:
401+
402+
```bash
403+
# Using kcadm.sh inside container
404+
podman exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
405+
--server http://localhost:8080 --realm master --user admin --password admin
406+
podman exec keycloak /opt/keycloak/bin/kcadm.sh update realms/master -s sslRequired=NONE
407+
```
408+
409+
The testing script (`test-auth.sh`) handles this automatically.
410+
265411
### Enable Debug Logging
266412

267413
Add to `application.properties` or set as environment variable:
@@ -270,9 +416,16 @@ Add to `application.properties` or set as environment variable:
270416
quarkus.log.category."io.quarkus.oidc".level=DEBUG
271417
```
272418

419+
Or run the testing script with debug flag:
420+
421+
```bash
422+
./scripts/test-auth.sh --debug
423+
```
424+
273425
## Additional Resources
274426

275427
- [Quarkus OIDC Guide](https://quarkus.io/guides/security-openid-connect)
428+
- [Quarkus OIDC Bearer Token Authentication](https://quarkus.io/guides/security-oidc-bearer-token-authentication)
276429
- [Quarkus Configuring Well-Known OpenID Connect Providers](https://quarkus.io/guides/security-openid-connect-providers)
277430
- [Keycloak Documentation](https://www.keycloak.org/documentation)
278431
- [GitHub OAuth Apps](https://docs.github.com/en/developers/apps/building-oauth-apps)

docs/development.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ Or, if you don't have GraalVM installed, you can run the native executable build
6161

6262
You can then execute your native executable with: `./target/agent-morpheus-client-1.0.0-SNAPSHOT-runner`
6363

64+
### Building with profiles
65+
66+
Some Quarkus properties are **build-time only** and cannot be changed at runtime. When building for a specific deployment target, include the profile:
67+
68+
```shell
69+
# For external-idp deployments (Keycloak, Google, etc.)
70+
./mvnw package -Dnative -Dquarkus.profile=external-idp
71+
72+
# For prod deployments (OpenShift OAuth) - default
73+
./mvnw package -Dnative
74+
```
75+
76+
**Important:** The CI/CD pipeline builds a universal image without a specific profile. Runtime profile selection via `QUARKUS_PROFILE` works for most configurations, but build-time properties (like `@IfBuildProfile` annotations) are fixed at compile time.
77+
6478
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/maven-tooling>.
6579

6680
## Related Guides

scripts/test-auth.sh

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
5959
APP_NAMESPACE=""
6060
KC_BASE_URL=""
6161
KC_TOKEN=""
62+
DEBUG="${DEBUG:-false}"
6263

6364
# Colors for output
6465
readonly RED='\033[0;31m'
@@ -86,6 +87,12 @@ print_info() {
8687
echo "$1"
8788
}
8889

90+
print_debug() {
91+
if [ "$DEBUG" = "true" ]; then
92+
echo -e "${YELLOW}[DEBUG] $1${NC}"
93+
fi
94+
}
95+
8996
show_help() {
9097
cat << EOF
9198
Authentication Testing Script
@@ -94,17 +101,19 @@ Usage: $0 [OPTIONS]
94101
95102
Options:
96103
--help, -h Show this help message
104+
--debug, -d Enable debug output (shows curl responses, URLs, etc.)
97105
98106
Environment Variables:
99-
CONTAINER_ENGINE Container runtime: podman or docker (auto-detected)
100-
APP_SERVICE_NAME Kubernetes deployment name (default: exploit-iq-client)
101107
APP_CLIENT_ID OIDC client ID (default: exploit-iq-client)
102108
APP_CLIENT_SECRET OIDC client secret (default: example-credentials)
103-
KC_LOCAL_PORT Local Keycloak port (default: 8190)
109+
APP_SERVICE_NAME Kubernetes deployment name (default: exploit-iq-client)
110+
CONTAINER_ENGINE Container runtime: podman or docker (auto-detected)
111+
DEBUG Enable debug mode (true/false, default: false)
112+
KC_ADMIN_PASS Keycloak admin password (default: admin)
113+
KC_ADMIN_USER Keycloak admin username (default: admin)
104114
KC_CONTAINER_NAME Keycloak container name (default: keycloak-standalone)
105115
KC_IMAGE Keycloak container image (default: quay.io/keycloak/keycloak:26.4)
106-
KC_ADMIN_USER Keycloak admin username (default: admin)
107-
KC_ADMIN_PASS Keycloak admin password (default: admin)
116+
KC_LOCAL_PORT Local Keycloak port (default: 8190)
108117
KC_OCP_NAMESPACE OpenShift namespace for Keycloak (default: keycloak-dev)
109118
110119
Scenarios:
@@ -213,10 +222,19 @@ setup_local_keycloak() {
213222
-e KC_HEALTH_ENABLED=true \
214223
-e KC_METRICS_ENABLED=true \
215224
-e KC_HTTP_MANAGEMENT_HEALTH_ENABLED=false \
225+
-e KC_HOSTNAME=localhost \
226+
-e KC_HTTP_ENABLED=true \
216227
"${KC_IMAGE}" start-dev >/dev/null
217228

218229
KC_BASE_URL="http://localhost:${KC_LOCAL_PORT}"
219230
wait_for_keycloak_health "$KC_BASE_URL"
231+
232+
# Disable SSL requirement for local development (Keycloak 26.x requires this)
233+
print_info "Configuring Keycloak for HTTP access..."
234+
${CONTAINER_ENGINE} exec "${KC_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh config credentials \
235+
--server http://localhost:8080 --realm master --user "${KC_ADMIN_USER}" --password "${KC_ADMIN_PASS}" >/dev/null
236+
${CONTAINER_ENGINE} exec "${KC_CONTAINER_NAME}" /opt/keycloak/bin/kcadm.sh update realms/master -s sslRequired=NONE >/dev/null
237+
print_success "Keycloak configured for HTTP"
220238
echo ""
221239
}
222240

@@ -294,15 +312,24 @@ wait_for_keycloak_health() {
294312
# ==============================================================================
295313

296314
get_admin_token() {
297-
KC_TOKEN=$(curl -k -s -X POST "${KC_BASE_URL}/realms/master/protocol/openid-connect/token" \
315+
local token_url="${KC_BASE_URL}/realms/master/protocol/openid-connect/token"
316+
print_debug "Token URL: $token_url"
317+
print_debug "Admin user: ${KC_ADMIN_USER}"
318+
319+
local response
320+
response=$(curl -k -s -X POST "$token_url" \
298321
-d "username=${KC_ADMIN_USER}" \
299322
-d "password=${KC_ADMIN_PASS}" \
300323
-d "grant_type=password" \
301-
-d "client_id=admin-cli" \
302-
| grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
324+
-d "client_id=admin-cli")
325+
326+
print_debug "Token response: $response"
327+
328+
KC_TOKEN=$(echo "$response" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
303329

304330
if [ -z "$KC_TOKEN" ]; then
305331
print_error "Failed to obtain Keycloak admin token"
332+
print_error "Response: $response"
306333
exit 1
307334
fi
308335
}
@@ -319,12 +346,12 @@ configure_keycloak() {
319346
-H "Authorization: Bearer $KC_TOKEN" || true
320347
print_success "done"
321348

322-
# Create realm
349+
# Create realm (with sslRequired=NONE for local development)
323350
echo -n "Creating realm '${KC_REALM}'... "
324351
curl -k -s -o /dev/null -X POST "${KC_BASE_URL}/admin/realms" \
325352
-H "Authorization: Bearer $KC_TOKEN" \
326353
-H "Content-Type: application/json" \
327-
-d "{\"realm\": \"${KC_REALM}\", \"enabled\": true}"
354+
-d "{\"realm\": \"${KC_REALM}\", \"enabled\": true, \"sslRequired\": \"NONE\"}"
328355
print_success "done"
329356

330357
sleep 1
@@ -343,7 +370,7 @@ configure_keycloak() {
343370
\"webOrigins\": [\"${app_url}\"],
344371
\"publicClient\": false,
345372
\"standardFlowEnabled\": true,
346-
\"directAccessGrantsEnabled\": false,
373+
\"directAccessGrantsEnabled\": true,
347374
\"attributes\": {
348375
\"post.logout.redirect.uris\": \"${app_url}/logged-out\"
349376
}
@@ -739,11 +766,20 @@ scenario_direct_google() {
739766

740767
main() {
741768
# Parse arguments
742-
case "${1:-}" in
743-
--help|-h)
744-
show_help
745-
;;
746-
esac
769+
while [[ $# -gt 0 ]]; do
770+
case "$1" in
771+
--help|-h)
772+
show_help
773+
;;
774+
--debug|-d)
775+
DEBUG=true
776+
shift
777+
;;
778+
*)
779+
shift
780+
;;
781+
esac
782+
done
747783

748784
check_dependencies
749785
cd "$PROJECT_ROOT"

0 commit comments

Comments
 (0)