Simple OIDC provider with Keycloak

Tomáš Sapák
FAUN — Developer Community 🐾
6 min readJan 16, 2024

--

Are you developing an application and want to use 3rd party identity providers like Google or Facebook instead of implementing your own credential database? Or are you running for your company systems like LDAP or Kerberos, but you would like to authenticate your applications with a more modern protocol like OIDC? Then you are at the right place.

When you implement OIDC protocol in your application, you can choose from a wide range of public OIDC providers¹ or OIDC provider implementations² for on-premise installations. In most cases, you’ll select a single implementation, which is the least time-consuming for you (you want to develop your application, not burn time setting stuff up). Once your application is finished, you’ll want to invite not only your colleagues with their company Microsoft accounts but also your friends using Google accounts. So after six beers with enough courage to show your opus magnum, you’ll let your friend log in with his Google account and … It’s not working. What the hell? Shouldn’t your application be compatible with any OIDC provider by following OIDC specification?

Well, it’s not that easy. OIDC specification is sadly not that strict. For example, there are no requirements for access token format. Microsoft is using JWT³ tokens, but Google, on the other hand, is using opaque⁴ tokens. If you implement OIDC, you usually won’t code everything from scratch but use existing libraries instead. And those libraries tend to depend on specific implementations.

When we started to help our customers run the KYPO Cyber Range Platform⁵ with their Identity Providers, those issues started to appear. Microsoft was the only compatible public provider, but it still had a few limitations.

Keycloak

After some research, I stumbled upon Keycloak, which was perfect for our use case and quickly became one of my favorite Open Source projects. Some of Keycloak’s key feature:

  • Support for both local and external users via LDAP or Kerberos
  • OIDC and SAML protocols
  • Social login — This is a killer feature. Keycloak is able to act as a proxy OIDC/SAML provider to not only any other OIDC provider but also to OAUTH providers like Facebook or Twitter. Since OAUTH is just an authorization protocol, authentication with these providers is generally more provider-dependent than with OIDC and, thus, more complex to implement directly.

Deployment

Keycloak is not only an excellent software, but thanks to the Kubernetes Operator support, it’s also super easy to deploy. The only requirements are Kubernetes cluster (e.g., public cloud instance or k3s⁶) and PostgreSQL database (again either public provider-managed or PostgreSQL operator⁷). In the following text, I’ll show you all Kubernetes resources needed to be created to have a working Keycloak deployment. They are all from chart kypo-keycloak⁸.

Installation

Keycloak operator supports two CDRs — Keycloak server and realm import. Realm (scoped set of all Keycloak resources) import is not practical:

  • You need an existing Keycloak installation with settings to export — it’s not suitable for new deployments.
  • Realm import is a one-time thing, which is not really a “DevOps” way of managing resources.

Luckily, Keycloak additionally supports something called Legacy operator, which is able to manage most of the basic resources Keycloak offers.

To install all necessary components, run the following commands:

kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/22.0.3/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/22.0.3/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/22.0.3/kubernetes/kubernetes.yml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/crds/legacy.k8s.keycloak.org_externalkeycloaks_crd.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/crds/legacy.k8s.keycloak.org_keycloakclients_crd.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/crds/legacy.k8s.keycloak.org_keycloakrealms_crd.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/crds/legacy.k8s.keycloak.org_keycloakusers_crd.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/role.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/role_binding.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/service_account.yaml
kubectl apply -n kypo -f https://raw.githubusercontent.com/keycloak/keycloak-realm-operator/main/deploy/operator.yaml

Resources

Keycloak

The first resource we’ll create is Keycloak and Secret for the initial admin user.

apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.name }}-initial-admin
data:
username: {{ .Values.keycloakAdmin | b64enc }}
password: {{ .Values.keycloakPassword | b64enc }}
type: kubernetes.io/basic-auth
---
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: {{ .Values.name }}
labels: {{- include "common.labels" (dict "name" .Values.name "chart" $.Chart "release" $.Release "extraLabels" $.extraLabels) | nindent 4 }}
spec:
instances: 1
db:
vendor: postgres
host: {{ $.Values.global.postgres.serviceName }}
port: {{ $.Values.global.postgres.port }}
database: {{ .Values.db.database }}
usernameSecret:
name: {{ .Values.name }}-secrets
key: username
passwordSecret:
name: {{ .Values.name }}-secrets
key: password
http:
httpEnabled: true
httpPort: 8080
ingress:
enabled: false
hostname:
hostname: {{ $.Values.global.headHost }}
additionalOptions:
- name: proxy
value: edge
- name: hostname-path
value: /keycloak
- name: http-relative-path
value: /keycloak

Important parameters:

  • db — access settings to PostgreSQL database
  • ingress — we’ll use external ingress instead of the one provided by the operator
  • additionalOptions —settings connected with Keycloak behind a reverse proxy on a specific path

Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Values.name }}-ingress
labels: {{- include "common.labels" (dict "name" .Values.name "chart" $.Chart "release" $.Release "extraLabels" .Values.extraLabels) | nindent 4 }}
spec:
tls:
- hosts:
- {{ $.Values.global.headHost }}
secretName: {{ $.Values.global.tlsSecretName }}
rules:
{{- if mustRegexMatch "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.){3}(25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)$" $.Values.global.headHost }}
- http:
{{- else }}
- host: {{ $.Values.global.headHost }}
http:
{{- end }}
paths:
- path: /keycloak
pathType: Prefix
backend:
service:
name: keycloak-service
port:
number: 8080

ExternalKeycloak

Settings for the legacy operator used for connecting to the Keycloak running by a new operator (this step wouldn’t be necessary if everything was managed by the single operator).

apiVersion: legacy.k8s.keycloak.org/v1alpha1
kind: ExternalKeycloak
metadata:
name: external-keycloak
labels:
app: external-sso
spec:
url: http://keycloak-service:8080
contextRoot: /keycloak/
---
apiVersion: v1
kind: Secret
metadata:
name: credential-external-keycloak
type: Opaque
stringData:
ADMIN_USERNAME: {{ .Values.keycloakAdmin }}
ADMIN_PASSWORD: {{ .Values.keycloakPassword }}

KeycloakRealm

CDR for management of the KeycloakRealm. The only CDR that is not able to do the updates. So be careful on the first deployment because any changes can be made later only in GUI or, for example, with Terraform.

apiVersion: legacy.k8s.keycloak.org/v1alpha1
kind: KeycloakRealm
metadata:
name: keycloakrealm
labels:
app: external-sso
spec:
instanceSelector:
matchLabels:
app: external-sso
realm:
id: "KYPO"
realm: "KYPO"
enabled: True
displayName: "KYPO Realm"

If you need custom OIDC client scopes, this is also done on the realm level. Just beware. Specifying a single custom scope will remove all default scopes. For the full usage, check the kypo-keycloak chart⁹. One of the useful features is that you can map custom attributes to the claims. Code for mapping the email address to the sub:

      - name: openid
protocol: openid-connect
protocolMappers:
- name: sub
protocol: openid-connect
protocolMapper: oidc-usermodel-property-mapper
config:
user.attribute: email
claim.name: sub
jsonType.label: String
id.token.claim: 'true'
access.token.claim: 'true'
userinfo.token.claim: 'true'

KeycloakClient

KeycloakClient manages the OIDC client.

apiVersion: legacy.k8s.keycloak.org/v1alpha1
kind: KeycloakClient
metadata:
name: kypo-client
labels:
app: external-sso
spec:
realmSelector:
matchLabels:
app: external-sso
client:
attributes:
pkce.code.challenge.method: S256
post.logout.redirect.uris: "https://{{ $.Values.global.headHost }}/logout-confirmed"
clientId: {{ range $.Values.global.oidcProviders }}{{- if contains "keycloak" .url }}{{ .clientId }}{{- end }}{{- end }}
clientAuthenticatorType: client-secret
defaultClientScopes:
- email
- profile
- openid
directAccessGrantsEnabled: true
implicitFlowEnabled: false
name: "KYPO-Client"
optionalClientScopes:
- offline_access
protocol: openid-connect
publicClient: true
redirectUris:
- "https://{{ $.Values.global.headHost }}"
- "https://{{ $.Values.global.headHost }}/index.html"
- "https://{{ $.Values.global.headHost }}/silent-refresh.html"
standardFlowEnabled: true

KeycloakUser

Finally, KeycloakUser manages local Keycloak users.

apiVersion: legacy.k8s.keycloak.org/v1alpha1
kind: KeycloakUser
metadata:
name: {{ .keycloakUsername | replace "@" "-" }}
labels:
app: external-sso
spec:
realmSelector:
matchLabels:
app: external-sso
user:
username: {{ .keycloakUsername }}
firstName: {{ .givenName }}
lastName: {{ .familyName }}
email: {{ .email }}
enabled: True
emailVerified: False
id: {{ .email }}
credentials:
- type: "password"
value: {{ .keycloakPassword }}
realmRoles:
- offline_access

That’s it. These are all the resources manageable by Keycloak/legacy operator. Terraform Keycloak Provider¹⁰ supports additional resources, although not all social identity providers are supported.

Conclusion

Keycloak is probably the best solution for modern authentication & authorization. It integrates very well with identity systems like LDAP or Kerberos and can unify authentication via a wide range of social identity providers. Thanks to Kubernetes Operators, deployment and management of Keycloak service are like a walk in the park.

PS: KYPO CRP integrated Keycloak in the 23.12 release¹¹.

--

--

DevOps engineer, automation, and orchestration enthusiast. Love working with AWS and OpenStack.