If you’re running Apicurio Registry on OpenShift, you might be wondering: can I use OpenShift’s built-in OAuth server for authentication instead of deploying a separate identity provider like Keycloak? The answer is yes — with some caveats. In this post, I’ll walk you through how we got it working, what the limitations are, and the technical details behind the integration.
The Challenge
OpenShift ships with a built-in OAuth server that handles user authentication for the platform. However, it is not a standard OpenID Connect (OIDC) provider:
- It does not expose
/.well-known/openid-configuration - It issues opaque tokens (
sha256~...), not JWTs - It has no JWKS endpoint and no RFC 7662 token introspection endpoint
Apicurio Registry uses Quarkus OIDC for authentication, which normally expects a fully OIDC-compliant server. So how do we bridge the gap?
The Solution: verify-access-token-with-user-info
The trick is to combine three components:
-
JWKS from the Kubernetes API server — The K8s API server exposes a JWKS endpoint at
/openid/v1/jwks. Quarkus OIDC requires either a JWKS or introspection path to initialize. We point it to the K8s JWKS purely to satisfy this startup requirement. -
OpenShift’s User API (
/apis/user.openshift.io/v1/users/~) — When called with a valid Bearer token, this endpoint returns the authenticated user’s identity including their OpenShift groups. When called with an invalid token, it returns 401. -
Quarkus
verify-access-token-with-user-info=true— This tells Quarkus to validate opaque access tokens by sending a GET request with the Bearer token to the userinfo endpoint. If the endpoint returns 200, the token is valid; otherwise, it’s rejected.
Here’s the flow:
+----------------------+
Client Request | Apicurio Registry |
with Bearer token ---> | (Quarkus OIDC) |
| |
| 1. Sends GET with |
| Bearer token to |
| userinfo endpoint |
+----------+-----------+
|
v
+----------------------+
| K8s API Server |
| /apis/user.openshift |
| .io/v1/users/~ |
| |
| Valid token -> 200 |
| + user info (with |
| groups array) |
| |
| Invalid token -> 401 |
+----------------------+
The User API response includes an OpenShift groups array at the top level. By creating OpenShift groups that match Apicurio role names, we map group membership directly to Registry roles.
Step-by-Step Setup
1. Install the Apicurio Registry Operator
First, deploy the operator to your cluster:
kubectl create namespace apicurio-registry-operator
kubectl apply -f apicurio-registry-operator.yaml -n apicurio-registry-operator
kubectl wait --for=condition=available deployment/apicurio-registry-operator \
-n apicurio-registry-operator --timeout=300s
2. Grant Anonymous Access to the JWKS Endpoint
The K8s API server JWKS endpoint is restricted by default. Since Quarkus OIDC fetches JWKS without authentication, we need to open it up. The JWKS contains only public keys, so there’s no security risk:
kubectl apply -f - <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: apicurio-jwks-anonymous-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:service-account-issuer-discovery
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:unauthenticated
EOF
3. Create an OAuthClient Resource
Register Apicurio Registry as a client of OpenShift’s OAuth server:
CLIENT_SECRET=$(openssl rand -base64 32 | tr -d '=' | head -c 32)
echo "Client secret: $CLIENT_SECRET"
apiVersion: oauth.openshift.io/v1
kind: OAuthClient
metadata:
name: apicurio-registry
grantMethod: auto
secret: "<CLIENT_SECRET>"
redirectURIs:
- "https://<registry-app-route>"
- "https://<registry-ui-route>"
- "http://<registry-app-route>"
- "http://<registry-ui-route>"
Tip: Include both http:// and https:// variants in redirectURIs to avoid 400 errors during OAuth flows.
4. Create OpenShift Groups for Role-Based Authorization
Create groups that map to Apicurio Registry roles:
oc adm groups new registry-admins
oc adm groups new registry-developers
oc adm groups new registry-readers
oc adm groups add-users registry-admins <admin-username>
oc adm groups add-users registry-developers <developer-username>
oc adm groups add-users registry-readers <reader-username>
5. Deploy the Registry
This is where it all comes together. The CR configures Quarkus OIDC to work with OpenShift’s non-OIDC-compliant OAuth server:
apiVersion: registry.apicur.io/v1
kind: ApicurioRegistry3
metadata:
name: apicurio-registry
namespace: apicurio-registry
spec:
app:
host: apicurio-registry-app-apicurio-registry.apps.<cluster-domain>
auth:
enabled: true
appClientId: apicurio-registry
uiClientId: apicurio-registry
authServerUrl: "https://oauth-openshift.apps.<cluster-domain>"
anonymousReadsEnabled: true
tls:
tlsVerificationType: "none"
authz:
enabled: true
readAccessEnabled: true
ownerOnlyEnabled: false
groupAccessEnabled: false
roles:
source: token
admin: "registry-admins"
developer: "registry-developers"
readOnly: "registry-readers"
adminOverride:
enabled: true
from: token
type: role
role: "registry-admins"
env:
# Disable OIDC auto-discovery
- name: QUARKUS_OIDC_DISCOVERY_ENABLED
value: "false"
# OAuth endpoints on the OpenShift OAuth server
- name: QUARKUS_OIDC_AUTHORIZATION_PATH
value: "/oauth/authorize"
- name: QUARKUS_OIDC_TOKEN_PATH
value: "/oauth/token"
# K8s API server JWKS (only for Quarkus startup initialization)
- name: QUARKUS_OIDC_JWKS_PATH
value: "https://api.<cluster-domain>:6443/openid/v1/jwks"
# OpenShift User API as the userinfo endpoint
- name: QUARKUS_OIDC_USER_INFO_PATH
value: "https://api.<cluster-domain>:6443/apis/user.openshift.io/v1/users/~"
# KEY SETTING: validate opaque tokens via the userinfo endpoint
- name: QUARKUS_OIDC_TOKEN_VERIFY_ACCESS_TOKEN_WITH_USER_INFO
value: "true"
# Extract OpenShift groups as roles
- name: QUARKUS_OIDC_ROLES_ROLE_CLAIM_PATH
value: "groups"
# Client credentials
- name: QUARKUS_OIDC_CREDENTIALS_SECRET
value: "<CLIENT_SECRET>"
ui:
host: apicurio-registry-ui-apicurio-registry.apps.<cluster-domain>
Important: The tls section under auth is required when auth is enabled. Omitting it causes a NullPointerException in the operator.
6. Verify It Works
Once the pods are running, you can test the full authorization matrix:
# Anonymous read (should work)
curl -sk http://<registry-app-route>/apis/registry/v3/system/info
# Unauthenticated write (should return 401)
curl -sk -X POST http://<registry-app-route>/apis/registry/v3/groups \
-H "Content-Type: application/json" -d '{"groupId": "test"}'
# Admin write (should return 200)
oc login -u testadmin -p testadmin123
TOKEN=$(oc whoami --show-token)
curl -sk -X POST http://<registry-app-route>/apis/registry/v3/groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"groupId": "admin-test"}'
# Reader write (should return 403)
oc login -u testreader -p testreader123
TOKEN=$(oc whoami --show-token)
curl -sk -X POST http://<registry-app-route>/apis/registry/v3/groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"groupId": "reader-test"}'
What Works and What Doesn’t
Authentication and role-based authorization work. Owner-based authorization and UI login do not.
| Feature | Status | Notes |
|---|---|---|
| Token validation | Works | Valid OpenShift OAuth tokens accepted, invalid tokens rejected |
| Anonymous reads | Works | When anonymousReadsEnabled: true |
| Auth enforcement | Works | Unauthenticated writes are blocked with 401 |
| Role-based authorization | Works | OpenShift groups mapped to Apicurio roles |
| Admin override | Works | Users in registry-admins get full admin access |
| Principal identity | Broken | Username is in metadata.name (nested), but Quarkus only reads top-level fields |
| Owner-based authorization | Broken | Depends on principal identity |
| UI login flow | Broken | UI requires OIDC discovery, which OpenShift OAuth doesn’t support |
Why Principal Identity Doesn’t Work
This is worth explaining in detail because it reveals an interesting limitation in Quarkus OIDC.
The K8s User API returns the username nested inside metadata.name:
{
"kind": "User",
"metadata": { "name": "testadmin" },
"groups": ["registry-admins", "system:authenticated"]
}
For opaque tokens validated via UserInfo, Quarkus extracts the principal with a flat JSON lookup:
userName = userInfo.getString(resolvedContext.oidcConfig().token().principalClaim().get());
This means getString("metadata/name") returns null — there’s no top-level key with that name. Interestingly, the findClaimValue() method used for role extraction supports /-separated path traversal, but this method is not used for principal extraction from UserInfo.
We tested every alternative:
metadata/name— empty (path not traversed)metadata.name— empty (not a valid key)metadata— 500 error (JsonObject can’t cast to JsonString)kind— returns “User” (proves top-level strings work, but useless)
The only top-level string fields in the response are kind and apiVersion, neither of which contains the username.
Troubleshooting Tips
If you run into issues, here are the most common problems:
- “Either ‘jwks-path’ or ‘introspection-path’ must be set” — Add the
QUARKUS_OIDC_JWKS_PATHenv var pointing to the K8s JWKS endpoint. - “Introspection path and verifyAccessTokenWithUserInfo are mutually exclusive” — Use
jwks-pathinstead ofintrospection-path. It’s only needed for initialization. - NullPointerException on TLS — Always include the
tlssection underauthwhen auth is enabled. - JWKS 403 — Create the ClusterRoleBinding for anonymous JWKS access.
- OAuth 400 Bad Request — Ensure redirect URIs match exactly, including protocol scheme.
- Users don’t get the expected role — Verify group membership with
oc get groupsand re-authenticate to get a new token.
Alternative: /oauth/info Endpoint
If you only need pure authentication without role-based authorization, there’s a simpler option. OpenShift’s OAuth server exposes /oauth/info, which validates Bearer tokens and returns basic token metadata — but no user identity or groups. Set QUARKUS_OIDC_USER_INFO_PATH to /oauth/info and skip the K8s User API entirely.
Wrapping Up
While this integration has limitations (no principal identity, no UI login, no owner-based authz), it provides a solid foundation for API-level authentication and role-based authorization using only OpenShift’s built-in infrastructure — no external identity provider required. For teams that primarily interact with Apicurio Registry through REST APIs and want to leverage their existing OpenShift user and group management, this is a practical solution.
We’d love to hear from you if you try this out or have ideas for improving the integration. The Quarkus OIDC limitation around nested principal claims is something we’re looking into upstream as well.
Happy registering!
