This guide covers authentication configuration for ExploitIQ Client, including OpenShift OAuth, external identity providers (Keycloak, AWS Cognito, Google, and others), and development setups.
ExploitIQ supports multiple authentication modes via Quarkus profiles:
| Profile | Use Case | Identity Provider |
|---|---|---|
prod |
OpenShift | OpenShift OAuth |
external-idp |
External identity providers | Keycloak, AWS Cognito, Google, Azure AD, Okta |
dev |
Local development | Keycloak DevServices (OIDC off by default) |
No Cognito-specific Quarkus profile is required. Point the existing external-idp profile at a Cognito User Pool (discovery, hybrid app type, and access-token role source already fit Cognito).
All profiles support both browser and API authentication:
| Method | Use Case | Flow |
|---|---|---|
| Browser | Web UI | Authorization Code Flow (redirects to IdP) |
| API | CLI, scripts, services, agent | Bearer JWT token in Authorization header |
Token acquisition differs by profile / IdP:
prod(OpenShift): Useoc whoami -tor ServiceAccount tokensexternal-idp(Keycloak): OIDC token endpoint with password or client_credentials grantexternal-idp(AWS Cognito): Hosted UI / managed login for browsers;client_credentialswith Basic auth for M2M (agent)dev: OIDC disabled by default; optional Keycloak DevServices when enabled
The default production configuration uses OpenShift's built-in OAuth server.
Create an OAuthClient resource in your OpenShift cluster:
apiVersion: oauth.openshift.io/v1
kind: OAuthClient
metadata:
name: exploit-iq-client
grantMethod: prompt
secret: <your-oauth-client-secret>
redirectURIs:
- "https://exploit-iq-client.<your-domain>"| Variable | Description | Example |
|---|---|---|
OPENSHIFT_DOMAIN |
OpenShift cluster domain | example.openshift.com |
OAUTH_CLIENT_SECRET |
Secret from OAuthClient resource | <your-secret> |
spec:
containers:
- name: exploit-iq-client
env:
- name: OPENSHIFT_DOMAIN
valueFrom:
secretKeyRef:
name: oauth-config
key: domain
- name: OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: oauth-config
key: secretFor API access in OpenShift, use your user token:
# After oc login
TOKEN=$(oc whoami -t)
curl -H "Authorization: Bearer $TOKEN" https://exploit-iq-client.apps.example.com/api/v1/reportsUse the external-idp profile to integrate with external OIDC providers.
Keycloak can be used standalone or as an identity broker for GitHub, Google, and other providers.
| Variable | Description | Example |
|---|---|---|
QUARKUS_PROFILE |
Must be external-idp |
external-idp |
QUARKUS_OIDC_AUTH_SERVER_URL |
Keycloak realm URL | https://keycloak.example.com/realms/<your-realm> |
QUARKUS_OIDC_CREDENTIALS_SECRET |
OIDC client secret | <your-client-secret> |
Note: The testing script uses quarkus as the default realm name. Replace with your actual realm name in production.
Create an OIDC client in Keycloak with the following settings:
{
"clientId": "exploit-iq-client",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "<your-client-secret>",
"redirectUris": ["https://your-app-url/*"],
"webOrigins": ["https://your-app-url"],
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true
}Important: directAccessGrantsEnabled: true is required for API authentication via password grant.
Required protocol mappers (add to client scope):
preferred_username: Mapsusernametopreferred_usernameclaimemail: Mapsemailtoemailclaimupn: Mapsusernametoupnclaim (fallback)
Connect directly to Google without Keycloak.
- Go to Google Cloud Console
- Create OAuth 2.0 Client ID (Web application)
- Add authorized redirect URI:
https://your-app-url/
| Variable | Description |
|---|---|
QUARKUS_PROFILE |
external-idp |
QUARKUS_OIDC_PROVIDER |
google |
QUARKUS_OIDC_CLIENT_ID |
Google Client ID |
QUARKUS_OIDC_CREDENTIALS_SECRET |
Google Client Secret |
env:
- name: QUARKUS_PROFILE
value: "external-idp"
- name: QUARKUS_OIDC_PROVIDER
value: "google"
- name: QUARKUS_OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: google-oauth
key: client-id
- name: QUARKUS_OIDC_CREDENTIALS_SECRET
valueFrom:
secretKeyRef:
name: google-oauth
key: client-secretUse the external-idp profile with an AWS Cognito User Pool for browser login and (with extra config) agent M2M bearer tokens.
Cognito differs from Keycloak in important ways:
| Topic | Cognito behavior |
|---|---|
| Human roles | JWT cognito:groups claim (group names must match ExploitIQ roles exactly) |
| M2M tokens | client_credentials tokens have no cognito:groups; authorize via OAuth2 scope → role mapping |
| Token endpoint | https://{domain}.auth.{region}.amazoncognito.com/oauth2/token |
| Client credentials | Requires HTTP Basic auth (client_id:client_secret), not body-only client auth |
| Browser scopes | openid, profile, email (custom Resource Server scopes are for M2M only) |
Role extraction is implemented in RoleMappingAugmentor (additive alongside OpenShift groups and Keycloak realm_access / resource_access). Do not set quarkus.oidc.roles.role-claim-path=cognito:groups on the shared external-idp profile — that would break Keycloak on the same profile.
- User Pool in your region.
- Groups named exactly:
exploit-iq-adminexploit-iq-viewexploit-iq-prodsec- optionally
exploitiq-api-access(for human users that should act like the API service role)
- App client (confidential / client secret) for the web UI:
- Authorization code grant
- Callback / sign-out URLs: your app origin (include both with and without trailing slash if needed), e.g.
http://localhost:8080andhttp://localhost:8080/ - OpenID scopes:
openid,email,profile
- Cognito domain (Amazon Cognito domain prefix is enough; custom domain is optional).
- Users in the pool, assigned to the groups above; set a permanent password for local testing (Forgot password needs email/SES configured).
- For agent M2M (optional, separate from browser client if desired):
- Resource Server with a custom scope, e.g. identifier
exploitiq-resource-server, scopeexploitiq-api-access - App client with client_credentials grant and that custom scope enabled
- Resource Server with a custom scope, e.g. identifier
| Variable | Description | Example |
|---|---|---|
QUARKUS_PROFILE |
Use external-idp (locally prefer dev,external-idp so %dev defaults still apply) |
external-idp or dev,external-idp |
QUARKUS_OIDC_AUTH_SERVER_URL |
Cognito issuer URL (User Pool), not the Hosted UI domain and not .../.well-known/openid-configuration |
https://cognito-idp.eu-north-1.amazonaws.com/eu-north-1_AbCdEf123 |
QUARKUS_OIDC_CLIENT_ID |
App client ID | Cognito console → App clients |
QUARKUS_OIDC_CREDENTIALS_SECRET |
App client secret | Cognito console → App clients |
EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS |
Optional M2M mapping: comma-separated scope=role pairs |
exploitiq-resource-server/exploitiq-api-access=exploitiq-api-access |
NAMESPACE |
Required for service-account role string expansion | OpenShift namespace, or local-dev locally |
CREDENTIAL_ENCRYPTION_KEY |
32-byte key for credential store (unrelated to Cognito) | Deployment secret |
Discover endpoints automatically via:
https://cognito-idp.{region}.amazonaws.com/{user-pool-id}/.well-known/openid-configuration
Quarkus appends /.well-known/openid-configuration itself — set QUARKUS_OIDC_AUTH_SERVER_URL to the issuer only.
env:
- name: QUARKUS_PROFILE
value: "external-idp"
- name: QUARKUS_OIDC_AUTH_SERVER_URL
value: "https://cognito-idp.eu-north-1.amazonaws.com/eu-north-1_AbCdEf123"
- name: QUARKUS_OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: cognito-oidc
key: client-id
- name: QUARKUS_OIDC_CREDENTIALS_SECRET
valueFrom:
secretKeyRef:
name: cognito-oidc
key: client-secret
# Required for agent/M2M bearer tokens (client_credentials) — omit for browser-only
- name: EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS
value: "exploitiq-resource-server/exploitiq-api-access=exploitiq-api-access"export NAMESPACE=local-dev
export CREDENTIAL_ENCRYPTION_KEY='dev-test-key-must-be-32bytes-long!'
export QUARKUS_PROFILE=dev,external-idp
export QUARKUS_OIDC_AUTH_SERVER_URL='https://cognito-idp.{region}.amazonaws.com/{user-pool-id}'
export QUARKUS_OIDC_CLIENT_ID='{cognito-app-client-id}'
export QUARKUS_OIDC_CREDENTIALS_SECRET='{cognito-app-client-secret}'
# Optional M2M:
# export EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS='exploitiq-resource-server/exploitiq-api-access=exploitiq-api-access'
./mvnw quarkus:dev \
-Dquarkus.rest-client.exploit-iq.url=http://localhost:26466/generateOpen http://localhost:8080 — you should be redirected to Cognito managed login / Hosted UI.
Session cookies: After login, Quarkus stores an HttpOnly session cookie (typically q_session) on the app origin (localhost:8080). You will not see the Cognito JWT in document.cookie or as a readable Cognito token cookie. Check DevTools → Application → Cookies → http://localhost:8080.
- Confirm the user is a member of
exploit-iq-admin(orview/prodsec) in Cognito. - After login, APIs should return 200 (not 403).
- Quarkus logs should include:
Mapping user to role 'exploit-iq-admin' from source: Cognito Group. - Optional: decode the access token (not only the ID token) and confirm a
cognito:groupsarray with those exact names.
Cognito client_credentials access tokens do not include cognito:groups. The client authorizes them by mapping the token scope claim via exploitiq.security.oidc.scope-role-mappings (env: EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS) onto an allowed role such as exploitiq-api-access (already listed in exploitiq.security.service-account-roles).
Fetch a token (agent-side code lives in the vulnerability-analysis repo; this shows the Cognito contract):
COGNITO_DOMAIN="{prefix}.auth.{region}.amazoncognito.com" # from Cognito Domain, not cognito-idp issuer
CLIENT_ID="{m2m-app-client-id}"
CLIENT_SECRET="{m2m-app-client-secret}"
SCOPE="exploitiq-resource-server/exploitiq-api-access"
TOKEN=$(curl -s -X POST "https://${COGNITO_DOMAIN}/oauth2/token" \
-u "${CLIENT_ID}:${CLIENT_SECRET}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&scope=${SCOPE}" | jq -r .access_token)
curl -i -H "Authorization: Bearer ${TOKEN}" \
http://localhost:8080/api/v1/reportsExpect 200 when scope mapping is configured on the client. Without EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS, the token may authenticate but fail authorization (403).
Note: Implementing Cognito token fetch in the Python agent (AUTH_TYPE=cognito, COGNITO_DOMAIN, etc.) is outside this repository.
The same external-idp approach works with other OIDC-compliant providers:
| Provider | Auth Server URL |
|---|---|
| Azure AD | https://login.microsoftonline.com/{tenant}/v2.0 |
| Okta | https://dev-xxxxx.okta.com/oauth2/default |
| Auth0 | https://your-domain.auth0.com |
| AWS Cognito | See AWS Cognito above |
Note: GitHub does not support OIDC. Use Keycloak as an identity broker for GitHub authentication.
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.
Use the password grant to obtain a token for a specific user:
# Configuration (match your Keycloak setup)
KC_URL="http://localhost:8190" # Keycloak URL
KC_REALM="quarkus" # Realm name
CLIENT_ID="exploit-iq-client" # Client ID
CLIENT_SECRET="example-credentials" # Client secret
USERNAME="bruce" # User
PASSWORD="wayne" # Password
# Get user token (scope=openid is REQUIRED)
USER_TOKEN=$(curl -s -X POST \
"${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
-d "grant_type=password" \
-d "scope=openid profile email" | jq -r '.access_token')
# Verify token was obtained
echo "Token: ${USER_TOKEN:0:50}..."Important: The scope=openid profile email parameter is required. Without openid, the UserInfo endpoint will reject the token with "Missing openid scope" error.
Use the token in the Authorization header:
# List reports
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/reports
# Get specific report
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/reports/{id}For machine-to-machine communication with Keycloak, use the client credentials grant:
# Get service token
SERVICE_TOKEN=$(curl -s -X POST \
"${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \
-d "client_id=${SERVICE_CLIENT_ID}" \
-d "client_secret=${SERVICE_SECRET}" \
-d "grant_type=client_credentials" | jq -r '.access_token')
curl -H "Authorization: Bearer $SERVICE_TOKEN" \
http://localhost:8080/api/v1/reportsNote: Requires a separate Keycloak client configured for service accounts.
For AWS Cognito M2M (client_credentials + Basic auth + custom scope), see AWS Cognito — Agent / M2M bearer tokens.
The application validates JWT tokens by:
- Verifying the signature using JWKS from the IdP
- Checking token expiration
- Validating the issuer (
issclaim) - Fetching UserInfo to extract user details
Keycloak can act as an identity broker, allowing users to authenticate via external providers while maintaining centralized user management.
User → Application → Keycloak (Broker) → External IdP (GitHub/Google)
↓
Token Issuance
↓
Application
- Create GitHub OAuth App at GitHub Developer Settings
- Set callback URL:
https://<your-keycloak>/realms/<your-realm>/broker/github/endpoint - Configure in Keycloak: Identity Providers → Add GitHub
- Add mappers:
login→preferred_usernameemail→email
- Create Google OAuth Client at Google Cloud Console
- Set redirect URI:
https://<your-keycloak>/realms/<your-realm>/broker/google/endpoint - Configure in Keycloak: Identity Providers → Add Google
- Add mappers:
email→email
By default, authentication is disabled in the dev profile to simplify local development.
To enable OIDC and start Keycloak DevServices, run:
./mvnw quarkus:dev \
-Dquarkus.oidc.enabled=true \
-Dquarkus.keycloak.devservices.enabled=trueTest users are defined in src/test/resources/devservices/keycloak-realm.json, which is automatically imported.
Default users:
bruce/wayne(Admin)peter/parker(Viewer)miles/morales(Client Role Admin)gwen/stacy(Client Role Viewer)
For testing with an external Keycloak instance:
# Start Keycloak (use podman or docker)
podman run -d --name keycloak \
-p 8190:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
-e KC_HTTP_ENABLED=true \
-e KC_HOSTNAME=localhost \
quay.io/keycloak/keycloak:26.4 start-dev
# Start application (using 'quarkus' as example realm name)
./mvnw quarkus:dev \
-Dquarkus.profile=external-idp \
-Dquarkus.oidc.auth-server-url=http://localhost:8190/realms/quarkus \
-Dquarkus.oidc.credentials.secret=example-credentials \
-Dquarkus.keycloak.devservices.enabled=falseAn automated testing script is available for all authentication scenarios:
./scripts/test-auth.sh --helpThe script supports DevServices Keycloak, external Keycloak (with optional GitHub/Google brokers), and direct Google OIDC.
After running a scenario with Keycloak, test API authentication:
# 1. Get user token (uses bruce/wayne created by the script)
USER_TOKEN=$(curl -s -X POST \
"http://localhost:8190/realms/quarkus/protocol/openid-connect/token" \
-d "client_id=exploit-iq-client" \
-d "client_secret=example-credentials" \
-d "username=bruce" \
-d "password=wayne" \
-d "grant_type=password" \
-d "scope=openid profile email" | jq -r '.access_token')
# 2. Verify token obtained
[ -n "$USER_TOKEN" ] && echo "Token obtained" || echo "Failed to get token"
# 3. Call API with Bearer token
curl -H "Authorization: Bearer $USER_TOKEN" \
http://localhost:8080/api/v1/reportsThe application resolves a display / actor name with this priority (UserService):
emailclaim (primary)upnclaim (User Principal Name)metadata.name(OpenShift)preferred_usernamesubanonymous(fallback)
For browser sessions, Quarkus may use UserInfo; for pure bearer M2M calls, the JWT principal name (sub, often the Cognito app client id) is typically used. Ensure your IdP includes email (or another claim above) for human users when you care about UI display names.
The application implements a unified role mapping strategy in RoleMappingAugmentor. On every authenticated request it inspects the JWT (and, for OpenShift prod, UserInfo-backed claims) and grants only roles that appear in the configured allow-list (quarkus.http.auth.policy.role-policy.roles-allowed, which includes human roles plus exploitiq.security.service-account-roles).
Target human roles:
exploit-iq-admin: Admin accessexploit-iq-view: Read-only accessexploit-iq-prodsec: Product Security access
Service-account style roles (skip report owner checks when held): configured via exploitiq.security.service-account-roles, including OpenShift SA names and exploitiq-api-access.
OpenShift Groups from UserInfo / groups are mapped when the group name matches a target role:
- Group
exploit-iq-admin→exploit-iq-admin - Group
exploit-iq-view→exploit-iq-view - Group
exploit-iq-prodsec→exploit-iq-prodsec
Kubernetes ServiceAccount JWTs may also map via the kubernetes.io claim / sub when configured in the allow-list.
- Realm roles:
realm_access.roles - Client roles:
resource_access.{client-id}.rolesforexploit-iq-client
- Browser / user tokens:
cognito:groups— each group name that matches a target role is granted (e.g. Cognito groupexploit-iq-admin→ roleexploit-iq-admin). - M2M /
client_credentialstokens: no group claim; configureexploitiq.security.oidc.scope-role-mappings(envEXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGS) asscope=rolepairs, e.g.exploitiq-resource-server/exploitiq-api-access=exploitiq-api-access.
When Cognito env vars and scope mappings are unset, Cognito-specific paths are no-ops; OpenShift and Keycloak behavior is unchanged.
Cause: Missing protocol mappers in Keycloak.
Solution: Add email, preferred_username, and upn mappers to the client scope.
Cause: The redirect URI in the OAuth app doesn't match the application URL.
Solution:
- Ensure exact match including trailing slash:
https://your-app/ - Changes may take 5-15 minutes to propagate
Cause: Token missing openid scope or invalid token.
Solution:
- Ensure
scope=openid profile emailis included in token request - Verify token is not expired
- Check Keycloak logs for "Missing openid scope" error
Cause: Token validated but no matching ExploitIQ role was mapped.
Solution (Cognito browser):
- Confirm the user is in a Cognito group named exactly
exploit-iq-admin,exploit-iq-view, orexploit-iq-prodsec - Confirm the access token contains
cognito:groupswith those names - Check logs for
Mapping user to role ... from source: Cognito Group
Solution (Cognito M2M):
- Set
EXPLOITIQ_SECURITY_OIDC_SCOPE_ROLE_MAPPINGSto map your custom scope toexploitiq-api-access - Ensure the token
scopeclaim contains that exact scope string - Check logs for
Mapping user to role ... from source: Cognito M2M Scope
Cause: App client Hosted UI / OAuth settings incomplete.
Solution:
- Enable Authorization code grant
- Allow scopes
openid,email,profilefor browser clients - Set callback URLs to the exact app origin (with and without trailing
/) - Ensure a Cognito domain exists and managed login status is Available
Cause: Wrong QUARKUS_OIDC_AUTH_SERVER_URL.
Solution: Use the issuer only:
https://cognito-idp.{region}.amazonaws.com/{user-pool-id}
Do not append /.well-known/openid-configuration (Quarkus adds it). Do not use the {prefix}.auth.{region}.amazoncognito.com Hosted UI domain as auth-server-url.
Cause: Looking in the wrong place, or expecting a readable JWT cookie.
Solution: Quarkus sets an HttpOnly session cookie (often q_session) on the application origin. Check DevTools → Application → Cookies → http://localhost:8080 (not the Cognito domain). document.cookie will not show HttpOnly cookies.
Cause: Keycloak 26.x requires HTTPS by default, even in development.
Solution: For local development, set sslRequired=NONE on the realm:
# Using kcadm.sh inside container
podman exec keycloak /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master --user admin --password admin
podman exec keycloak /opt/keycloak/bin/kcadm.sh update realms/master -s sslRequired=NONEThe testing script (test-auth.sh) handles this automatically.
Add to application.properties or set as environment variable:
quarkus.log.category."io.quarkus.oidc".level=DEBUGOr run the testing script with debug flag:
./scripts/test-auth.sh --debug