Keycloak Identity and Access Management

Authentication and Authorization

1. Introduction to Keycloak

Keycloak is an open-source Identity and Access Management solution aimed at modern applications and services. It provides user federation, strong authentication, user management, fine-grained authorization, and more.

What is Keycloak?

Keycloak is a comprehensive identity management solution that provides Single Sign-On (SSO), Identity Brokering, Social Login, User Federation, and Authorization Services. It acts as an identity provider that authenticates users and provides identity tokens to applications.

Key Features:

  • Single Sign-On (SSO): Users authenticate once and access multiple applications
  • User Federation: Integrate with LDAP, Active Directory, and other providers
  • Social Login: Support for Google, Facebook, GitHub, and other providers
  • Authorization Services: Fine-grained permissions and policies
  • Admin Console: Web-based administration interface
  • Themes: Customizable login pages and admin console

Why Keycloak?

Benefits:

  • Open Source and Free
  • Standards Compliant (OAuth2, OpenID Connect, SAML)
  • Easy Integration
  • Scalable Architecture
  • Extensible

Use Cases:

  • Enterprise SSO Solutions
  • Microservices Authentication
  • Mobile App Authentication
  • API Protection
  • User Management

2. Core Concepts

Keycloak Architecture

graph TD A[Users] --> B[Keycloak Server] C[Clients/Applications] --> B B --> D[Identity Providers] B --> E[User Storage] B --> F[Token Services] F --> C

Main Components

graph LR A[Realm] --> B[Users] A --> C[Clients] A --> D[Roles] A --> E[Groups] A --> F[Identity Providers] B --> G[Credentials] C --> H[Client Scopes] D --> I[Permissions]

Core Concepts Explained:

  • Realm: An isolated namespace for managing identities and applications
  • User: An entity that can authenticate and access resources
  • Client: An application that requests authentication and authorization
  • Role: A permission or capability assigned to users or clients
  • Group: A collection of users for easier management
  • Identity Provider: External authentication sources (LDAP, AD, Social)

3. Installation and Setup

Docker Installation

# Pull Keycloak Docker image
docker pull quay.io/keycloak/keycloak:22.0.1

# Run Keycloak container
docker run -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:22.0.1 \
  start-dev

# Access Keycloak admin console
# http://localhost:8080/admin
# Username: admin
# Password: admin

Docker Compose Setup

version: '3.8'

services:
  keycloak:
    image: quay.io/keycloak/keycloak:22.0.1
    ports:
      - "8080:8080"
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: password
    command: start-dev
    depends_on:
      - postgres
      
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Standalone Installation

# Download Keycloak
wget https://github.com/keycloak/keycloak/releases/download/22.0.1/keycloak-22.0.1.tar.gz
tar -xzf keycloak-22.0.1.tar.gz
cd keycloak-22.0.1

# Start Keycloak
bin/kc.sh start-dev

# Or on Windows
bin\kc.bat start-dev

Installation Options:

  • Docker: Quick setup for development and testing
  • Standalone: Full installation with customization options
  • Kubernetes: Production deployment with Helm charts
  • Cloud: Managed services (Red Hat SSO, AWS, etc.)

4. Realm Setup and Configuration

Creating a New Realm

graph TD A[Login to Admin Console] --> B[Create Realm] B --> C[Configure Realm Settings] C --> D[Set Display Name] C --> E[Configure Login Settings] C --> F[Set Themes] C --> G[Configure Tokens] D --> H[Save Realm]

Realm Configuration Steps

  1. Login to Admin Console: Navigate to http://localhost:8080/admin
  2. Create Realm: Click "Add realm" and enter realm name
  3. Configure Settings: Set display name, enabled status
  4. Login Configuration: Enable/disable registration, reset password, remember me
  5. Themes: Select login, email, and admin themes
  6. Tokens: Configure access token lifespan, refresh tokens

Realm Settings JSON

{
  "id": "my-realm",
  "realm": "my-realm",
  "displayName": "My Application Realm",
  "enabled": true,
  "registrationAllowed": true,
  "registrationEmailAsUsername": false,
  "rememberMe": true,
  "verifyEmail": false,
  "loginWithEmailAllowed": true,
  "duplicateEmailsAllowed": false,
  "resetPasswordAllowed": true,
  "editUsernameAllowed": false,
  "sslRequired": "external",
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "ssoSessionMaxLifespan": 36000,
  "offlineSessionIdleTimeout": 2592000,
  "accessCodeLifespan": 60,
  "accessCodeLifespanUserAction": 300,
  "accessCodeLifespanLogin": 1800
}

Realm Import/Export

# Export realm
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/admin/realms/my-realm \
  -o my-realm.json

# Import realm
curl -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @my-realm.json \
  http://localhost:8080/admin/realms

5. Client Authentication

Client Types

graph LR A[Clients] --> B[Public Clients] A --> C[Confidential Clients] A --> D[Bearer-only Clients] B --> E[SPA Applications] B --> F[Mobile Apps] C --> G[Backend Services] C --> H[Web Applications] D --> I[API Services]

Creating a Confidential Client

# Using Keycloak Admin CLI
kcadm.sh config credentials --server http://localhost:8080 \
  --realm master --user admin --password admin

kcadm.sh create clients -r my-realm -s clientId=my-backend-client \
  -s enabled=true -s clientAuthenticatorType=client-secret \
  -s secret=my-client-secret -s 'redirectUris=["http://localhost:3000/*"]'

Client Configuration

{
  "clientId": "my-web-app",
  "name": "My Web Application",
  "description": "Frontend web application",
  "enabled": true,
  "clientAuthenticatorType": "client-secret",
  "secret": "my-client-secret",
  "redirectUris": [
    "http://localhost:3000/*",
    "https://myapp.com/*"
  ],
  "webOrigins": [
    "http://localhost:3000",
    "https://myapp.com"
  ],
  "bearerOnly": false,
  "consentRequired": false,
  "standardFlowEnabled": true,
  "implicitFlowEnabled": false,
  "directAccessGrantsEnabled": true,
  "serviceAccountsEnabled": false,
  "publicClient": false,
  "protocol": "openid-connect",
  "attributes": {
    "saml.assertion.signature": "false",
    "saml.force.post.binding": "false",
    "saml.multivalued.roles": "false",
    "saml.encrypt": "false",
    "oauth2.device.authorization.grant.enabled": "false",
    "backchannel.logout.revoke.offline.tokens": "false",
    "use.refresh.tokens": "true",
    "oidc.ciba.grant.enabled": "false",
    "backchannel.logout.session.required": "true",
    "client_credentials.use_refresh_token": "false",
    "acr.loa.map": "{}",
    "require.pushed.authorization.requests": "false",
    "tls.client.certificate.bound.access.tokens": "false",
    "display.on.consent.screen": "false",
    "token.response.type.bearer.lower-case": "false"
  }
}

Client Scopes

{
  "name": "profile",
  "description": "OpenID Connect built-in scope: profile",
  "protocol": "openid-connect",
  "attributes": {
    "include.in.token.scope": "true",
    "display.on.consent.screen": "true",
    "consent.screen.text": "${profileScopeConsentText}"
  },
  "protocolMappers": [
    {
      "name": "full name",
      "protocol": "openid-connect",
      "protocolMapper": "oidc-full-name-mapper",
      "consentRequired": false,
      "config": {
        "id.token.claim": "true",
        "access.token.claim": "true",
        "userinfo.token.claim": "true"
      }
    }
  ]
}

6. User Management

User Creation

# Create user via Admin CLI
kcadm.sh create users -r my-realm -s username=john.doe \
  -s enabled=true -s email=john.doe@example.com

# Set password
kcadm.sh update users/$USER_ID/reset-password -r my-realm \
  -s type=password -s value=Password123 -s temporary=false -n

User Representation

{
  "username": "john.doe",
  "email": "john.doe@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "enabled": true,
  "emailVerified": true,
  "createdTimestamp": 1634567890000,
  "totp": false,
  "credentials": [
    {
      "type": "password",
      "value": "Password123",
      "temporary": false
    }
  ],
  "requiredActions": [],
  "federatedIdentities": [],
  "attributes": {
    "department": ["engineering"],
    "employeeId": ["EMP001"]
  },
  "groups": ["/developers", "/backend-team"]
}

Bulk User Import

[
  {
    "username": "alice.smith",
    "email": "alice.smith@example.com",
    "firstName": "Alice",
    "lastName": "Smith",
    "enabled": true,
    "credentials": [
      {
        "type": "password",
        "value": "Password123"
      }
    ],
    "groups": ["/marketing"]
  },
  {
    "username": "bob.johnson",
    "email": "bob.johnson@example.com",
    "firstName": "Bob",
    "lastName": "Johnson",
    "enabled": true,
    "credentials": [
      {
        "type": "password",
        "value": "Password123"
      }
    ],
    "groups": ["/sales"]
  }
]

User Federation with LDAP

{
  "name": "ldap-provider",
  "providerId": "ldap",
  "providerType": "org.keycloak.storage.UserStorageProvider",
  "parentId": "my-realm",
  "config": {
    "priority": ["0"],
    "enabled": ["true"],
    "cachePolicy": ["DEFAULT"],
    "evictionDay": [""],
    "evictionHour": [""],
    "evictionMinute": [""],
    "maxLifespan": [""],
    "batchSizeForSync": ["1000"],
    "fullSyncPeriod": ["-1"],
    "changedSyncPeriod": ["-1"],
    "lastSync": ["0"],
    "connectionUrl": ["ldap://ldap.example.com:389"],
    "usersDn": ["ou=people,dc=example,dc=com"],
    "bindDn": ["cn=admin,dc=example,dc=com"],
    "bindCredential": ["admin-password"],
    "searchScope": ["1"],
    "useTruststoreSpi": ["ldapsOnly"],
    "connectionPooling": ["true"],
    "pagination": ["true"],
    "allowKerberosAuthentication": ["false"],
    "debug": ["false"],
    "useKerberosForPasswordAuthentication": ["false"]
  }
}

7. Roles and Permissions

Role Hierarchy

graph TD A[Realm Roles] --> B[Administrator] A --> C[User] A --> D[Guest] B --> E[Manage Users] B --> F[Manage Clients] C --> G[View Profile] C --> H[Manage Account] D --> I[View Public Content]

Creating Realm Roles

# Create realm role via Admin CLI
kcadm.sh create roles -r my-realm -s name=admin -s description="Administrator role"

# Create composite role
kcadm.sh create roles -r my-realm -s name=super-user \
  -s description="Super user with multiple roles"

# Add child roles to composite role
kcadm.sh create roles-by-id/$ROLE_ID/composites -r my-realm \
  -s id=$CHILD_ROLE_ID

Client Roles

{
  "name": "app-admin",
  "description": "Application administrator",
  "composite": true,
  "clientRole": true,
  "containerId": "my-web-app",
  "attributes": {},
  "composites": {
    "client": {
      "my-web-app": [
        "manage-users",
        "view-reports",
        "configure-settings"
      ]
    }
  }
}

Assigning Roles to Users

# Assign realm role to user
kcadm.sh create users/$USER_ID/role-mappings/realm -r my-realm \
  -s name=admin -s id=$ROLE_ID

# Assign client role to user
kcadm.sh create users/$USER_ID/role-mappings/clients/$CLIENT_ID -r my-realm \
  -s name=app-admin -s id=$CLIENT_ROLE_ID

Fine-Grained Authorization

{
  "name": "customer-resource-policy",
  "type": "resource",
  "logic": "POSITIVE",
  "decisionStrategy": "UNANIMOUS",
  "config": {
    "resources": "[\"Customer Resource\"]",
    "applyPolicies": "[\"only-admin-policy\"]"
  }
}

8. Spring Boot Integration

Maven Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
</dependencies>

Application Properties

# Keycloak configuration
spring.security.oauth2.client.registration.keycloak.client-id=my-web-app
spring.security.oauth2.client.registration.keycloak.client-secret=my-client-secret
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak

spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/my-realm
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/my-realm

Security Configuration

package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/dashboard")
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            .logout(logout -> logout
                .logoutSuccessUrl("http://localhost:8080/realms/my-realm/protocol/openid-connect/logout?redirect_uri=http://localhost:8081/")
            );
        
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthorityPrefix("ROLE_");
        authoritiesConverter.setAuthoritiesClaimName("realm_access.roles");
        
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

Protected Controller

package com.example.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class UserController {
    
    @GetMapping("/user/profile")
    public String getUserProfile(@AuthenticationPrincipal OidcUser user) {
        return "Hello " + user.getGivenName() + " " + user.getFamilyName() + 
               " (Email: " + user.getEmail() + ")";
    }
    
    @GetMapping("/admin/dashboard")
    public String getAdminDashboard(Authentication authentication) {
        return "Admin Dashboard - User: " + authentication.getName();
    }
    
    @PostMapping("/user/data")
    public String saveUserData(@RequestBody String data, Authentication authentication) {
        // Check user permissions
        if (authentication.getAuthorities().stream()
                .anyMatch(auth -> auth.getAuthority().equals("ROLE_USER"))) {
            return "Data saved successfully by user: " + authentication.getName();
        }
        throw new SecurityException("Insufficient permissions");
    }
}

JWT Token Validation

package com.example.service;

import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.stereotype.Service;

@Service
public class TokenValidationService {
    
    private final JwtDecoder jwtDecoder;
    
    public TokenValidationService(JwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }
    
    public boolean validateToken(String token) {
        try {
            Jwt decodedJwt = jwtDecoder.decode(token);
            return !decodedJwt.getExpiresAt().isBefore(java.time.Instant.now());
        } catch (Exception e) {
            return false;
        }
    }
    
    public String getUsernameFromToken(String token) {
        try {
            Jwt decodedJwt = jwtDecoder.decode(token);
            return decodedJwt.getSubject();
        } catch (Exception e) {
            return null;
        }
    }
}

9. React Integration

React App Setup

# Create React app
npx create-react-app keycloak-react-app
cd keycloak-react-app

# Install Keycloak adapter
npm install keycloak-js

Keycloak Configuration

import Keycloak from 'keycloak-js';

const keycloak = new Keycloak({
  url: 'http://localhost:8080',
  realm: 'my-realm',
  clientId: 'my-web-app'
});

export default keycloak;

React App Integration

import React, { useState, useEffect } from 'react';
import keycloak from './keycloak';

function App() {
  const [authenticated, setAuthenticated] = useState(false);
  const [user, setUser] = useState(null);

  useEffect(() => {
    keycloak.init({ onLoad: 'check-sso' }).then((authenticated) => {
      setAuthenticated(authenticated);
      if (authenticated) {
        setUser(keycloak.tokenParsed);
      }
    }).catch(console.error);
  }, []);

  const login = () => {
    keycloak.login();
  };

  const logout = () => {
    keycloak.logout();
  };

  if (!authenticated) {
    return (
      <div className="App">
        <h1>Welcome to My App</h1>
        <button onClick={login}>Login</button>
      </div>
    );
  }

  return (
    <div className="App">
      <h1>Hello {user.preferred_username}!</h1>
      <p>Email: {user.email}</p>
      <p>First Name: {user.given_name}</p>
      <p>Last Name: {user.family_name}</p>
      <button onClick={logout}>Logout</button>
      
      <ProtectedComponent />
    </div>
  );
}

export default App;

Protected Component with API Calls

import React, { useState, useEffect } from 'react';
import keycloak from './keycloak';

function ProtectedComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch('http://localhost:8081/api/user/profile', {
        headers: {
          'Authorization': 'Bearer ' + keycloak.token,
          'Content-Type': 'application/json'
        }
      });
      
      if (response.ok) {
        const result = await response.text();
        setData(result);
      } else {
        throw new Error('Failed to fetch data');
      }
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    // Refresh token before API call
    keycloak.updateToken(30).then(refreshed => {
      if (refreshed) {
        console.log('Token refreshed');
      }
    }).catch(() => {
      console.log('Failed to refresh token');
    });
  }, []);

  return (
    <div>
      <h2>Protected Data</h2>
      <button onClick={fetchData} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch Protected Data'}
      </button>
      {data && <div><pre>{data}</pre></div>}
    </div>
  );
}

export default ProtectedComponent;

Token Refresh Hook

import { useState, useEffect } from 'react';
import keycloak from './keycloak';

export function useKeycloak() {
  const [initialized, setInitialized] = useState(false);
  const [authenticated, setAuthenticated] = useState(false);

  useEffect(() => {
    keycloak.init({ onLoad: 'check-sso' }).then((authenticated) => {
      setAuthenticated(authenticated);
      setInitialized(true);
    }).catch(console.error);

    // Set up token refresh interval
    const refreshInterval = setInterval(() => {
      keycloak.updateToken(70).catch(() => {
        console.log('Failed to refresh token');
      });
    }, 60000); // Refresh every minute

    return () => clearInterval(refreshInterval);
  }, []);

  return { initialized, authenticated, keycloak };
}

10. Best Practices

Security Best Practices

Token Security

Short-lived tokens with refresh rotation

Password Policies

Strong password requirements and MFA

Session Management

Proper session timeouts and cleanup

Production Deployment

graph TD A[Load Balancer] --> B[Keycloak Node 1] A --> C[Keycloak Node 2] A --> D[Keycloak Node N] B --> E[Shared Database] C --> E D --> E B --> F[Cache Cluster] C --> F D --> F

Configuration Best Practices

# Production configuration
# Enable HTTPS
proxy=edge
http-enabled=false

# Database configuration
db=postgres
db-url=jdbc:postgresql://postgres:5432/keycloak
db-username=keycloak
db-password=${KEYCLOAK_DB_PASSWORD}

# Cache configuration
cache=ispn
cache-stack=kubernetes

# Logging
log-level=INFO,org.keycloak.events=DEBUG

# Security
ssl-required=external
brute-force-protected=true
failure-factor=30

# Performance
transaction-xa-enabled=false
spi-user-cache-infinispan-max-lifespan=900000

Monitoring and Logging

{
  "logging": {
    "pattern": "%d{yyyy-MM-dd HH:mm:ss} %-5p [%c] (%t) %s%e%n",
    "console": {
      "enabled": true,
      "level": "INFO"
    },
    "file": {
      "enabled": true,
      "level": "DEBUG",
      "path": "/var/log/keycloak/keycloak.log"
    }
  },
  "metrics": {
    "enabled": true,
    "micrometer": {
      "enabled": true
    }
  },
  "events": {
    "enabled": true,
    "admin": {
      "enabled": true
    },
    "login": {
      "enabled": true
    }
  }
}

Essential Best Practices:

  • Token Lifespan: Short access tokens (5-30 min), longer refresh tokens
  • Password Security: Strong policies, hashing algorithms, breach detection
  • MFA Implementation: Multi-factor authentication for sensitive operations
  • Regular Audits: Monitor logs, events, and security incidents
  • Backup Strategy: Regular backups of realms and configurations
  • Update Management: Keep Keycloak updated with security patches