Skip to content

Authenticate with OIDC

This tutorial walks you through configuring Kannika Armory to authenticate users via OpenID Connect (OIDC), using Keycloak as the identity provider.

A self-contained lab environment is available with Keycloak and Kannika Armory pre-configured.

A realm is a namespace in Keycloak that holds users, clients, and configuration. A client represents an application that can request authentication.

Create a realm JSON file with a client for Kannika Armory:

kannika-realm.json
{
"realm": "kannika",
"enabled": true,
"sslRequired": "none",
"clients": [
{
"clientId": "kannika-armory",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["http://localhost:8080/*"],
"webOrigins": ["http://localhost:8080"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:8080/*"
}
}
]
}

The realm name appears in all OIDC endpoint URLs. Point the console Helm values to these endpoints:

values.yaml
console:
config:
security:
enabled: true
oidc:
enabled: true
clientId: kannika-armory
scope: openid profile email
authEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/auth
tokenEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/token
logoutEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/logout

The clientId must match the clientId in the realm JSON. The endpoints must be reachable from the user’s browser. These URLs are used client-side to redirect the user to Keycloak for login.

Configure the API to validate tokens from the same realm:

values.yaml
api:
config:
security:
enabled: true
oidc:
issuerUri: http://keycloak:8280/realms/kannika

The issuerUri must be reachable from the API pod inside Kubernetes. The API uses it to fetch the OIDC discovery metadata and validate tokens.

The API validates the aud (audience) claim in the access token to ensure the token was issued for the correct client. By default, Keycloak does not include the client ID in the aud claim of the access token.

Add an oidc-audience-mapper protocol mapper to the client in the realm JSON:

kannika-realm.json - clients[0].protocolMappers
{
"name": "audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"included.client.audience": "kannika-armory",
"id.token.claim": "false",
"access.token.claim": "true"
}
}

Then tell the API which audience to expect:

values.yaml
api:
config:
security:
oidc:
audience: kannika-armory

The audience value must match included.client.audience in the mapper.

The console shows the logged-in user’s name by reading a claim from the token. By default, Keycloak does not include a name claim. You need a protocol mapper to produce it.

Add a profile client scope with an oidc-full-name-mapper to the realm JSON. This mapper combines the user’s firstName and lastName into a name claim. The name field in the mapper is just a label for Keycloak’s admin UI. The claim name (name) is hardcoded in the mapper type.

kannika-realm.json - clientScopes
{
"name": "profile",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "full name",
"protocolMapper": "oidc-full-name-mapper",
"config": {
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
}

Then tell the console which claim to use:

values.yaml
console:
config:
security:
oidc:
displayNameTokenClaim: name

The displayNameTokenClaim must match the claim name produced by the mapper, in this case name.

The API identifies the user in logs and audit trails using a claim from the token. Add an email client scope with a mapper that puts the user’s email address into the token:

kannika-realm.json - clientScopes
{
"name": "email",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "email",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "email",
"claim.name": "email",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
}

Then tell the API which claim to use as the principal:

values.yaml
api:
config:
security:
oidc:
principalClaimName: email

Assign both scopes as default client scopes so they are included for all clients:

kannika-realm.json
{
"defaultDefaultClientScopes": ["openid", "profile", "email"]
}

The console requests the offline_access scope to obtain refresh tokens. This keeps the user’s session alive without redirecting to Keycloak when the access token expires.

Add an offline_access client scope to the realm JSON and register it as an optional client scope:

kannika-realm.json
{
"clientScopes": [
{
"name": "offline_access",
"protocol": "openid-connect"
}
],
"defaultOptionalClientScopes": ["offline_access"]
}

Users must have the offline_access realm role to be granted offline tokens. This is an optional scope because only the console needs it, not the API.

Add a user to the realm JSON with the attributes that the mappers reference:

kannika-realm.json - users
{
"username": "demo",
"enabled": true,
"firstName": "Demo",
"lastName": "User",
"email": "demo@example.com",
"realmRoles": ["default-roles-kannika", "offline_access"],
"credentials": [
{
"type": "password",
"value": "demo",
"temporary": false
}
]
}

The firstName and lastName are used by the oidc-full-name-mapper to produce the name claim. The email is used by the oidc-usermodel-attribute-mapper to produce the email claim. The realmRoles must include offline_access for the user to obtain refresh tokens. Imported users do not receive default realm roles automatically.

Import the realm using the --import-realm flag at startup, or through the Admin Console under Create RealmBrowse → select the JSON file.

Here is the full realm JSON file with all the configuration from the previous sections:

kannika-realm.json
{
"realm": "kannika",
"enabled": true,
"sslRequired": "none",
"clientScopes": [
{
"name": "openid",
"protocol": "openid-connect"
},
{
"name": "profile",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "full name",
"protocol": "openid-connect",
"protocolMapper": "oidc-full-name-mapper",
"config": {
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
},
{
"name": "email",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "email",
"claim.name": "email",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
},
{
"name": "offline_access",
"protocol": "openid-connect"
}
],
"defaultDefaultClientScopes": ["openid", "profile", "email"],
"defaultOptionalClientScopes": ["offline_access"],
"clients": [
{
"clientId": "kannika-armory",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["http://localhost:8080/*"],
"webOrigins": ["http://localhost:8080"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:8080/*"
},
"protocolMappers": [
{
"name": "audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"included.client.audience": "kannika-armory",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "demo",
"enabled": true,
"firstName": "Demo",
"lastName": "User",
"email": "demo@example.com",
"realmRoles": ["default-roles-kannika", "offline_access"],
"credentials": [
{
"type": "password",
"value": "demo",
"temporary": false
}
]
}
]
}

And the corresponding Helm values:

values.yaml
console:
config:
security:
enabled: true
oidc:
enabled: true
clientId: kannika-armory
scope: openid profile email
authEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/auth
tokenEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/token
logoutEndpoint: http://keycloak:8280/realms/kannika/protocol/openid-connect/logout
displayNameTokenClaim: name
api:
config:
security:
enabled: true
oidc:
issuerUri: http://keycloak:8280/realms/kannika
audience: kannika-armory
principalClaimName: email

For the full list of OIDC properties, refer to the security configuration reference.