From d0eb2567f318d10c245c42012e03a4696f0fde7e Mon Sep 17 00:00:00 2001 From: Omair Majid Date: Tue, 18 Nov 2025 18:38:10 -0500 Subject: [PATCH] Add a test for kerberos authentication Assisted-by: Claude Sonnet 4.5 (claude-sonnet-4-5@20250929) --- README.md | 5 +- kerberos-auth/aspnet-server/Program.cs | 62 +++++++ .../aspnet-server/aspnet-server.csproj | 14 ++ kerberos-auth/client/Program.cs | 64 +++++++ kerberos-auth/client/client.csproj | 8 + kerberos-auth/setup-kdc.sh | 134 ++++++++++++++ kerberos-auth/test.json | 14 ++ kerberos-auth/test.sh | 171 ++++++++++++++++++ 8 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 kerberos-auth/aspnet-server/Program.cs create mode 100644 kerberos-auth/aspnet-server/aspnet-server.csproj create mode 100644 kerberos-auth/client/Program.cs create mode 100644 kerberos-auth/client/client.csproj create mode 100755 kerberos-auth/setup-kdc.sh create mode 100644 kerberos-auth/test.json create mode 100755 kerberos-auth/test.sh diff --git a/README.md b/README.md index 28936f5..30ce7ef 100644 --- a/README.md +++ b/README.md @@ -12,5 +12,6 @@ $ git clone https://github.com/redhat-developer/dotnet-bunny && cd dotnet-bunny ### Dependencies -Dependencies(apk): babeltrace bash bash-completion binutils coreutils file findutils g++ jq libstdc++-dev lldb lttng-ust lttng-tools npm postgresql psqlodbc sed strace unixodbc zlib-dev -Dependencies(dnf): awk babeltrace bash-completion bc findutils gcc-c++ jq libstdc++-devel lttng-ust lttng-tools npm postgresql-odbc postgresql-server strace unixODBC /usr/bin/file /usr/bin/free /usr/bin/lldb /usr/bin/readelf /usr/bin/su which zlib-devel +Dependencies(apk): babeltrace bash bash-completion binutils coreutils file findutils g++ jq krb5-server krb5 libstdc++-dev lldb lttng-ust lttng-tools npm postgresql psqlodbc sed strace unixodbc zlib-dev +Dependencies(apt): krb5-kdc krb5-admin-server krb5-user +Dependencies(dnf): awk babeltrace bash-completion bc findutils gcc-c++ jq krb5-server krb5-workstation krb5-libs libstdc++-devel lttng-ust lttng-tools npm postgresql-odbc postgresql-server strace unixODBC /usr/bin/file /usr/bin/free /usr/bin/lldb /usr/bin/readelf /usr/bin/su which zlib-devel diff --git a/kerberos-auth/aspnet-server/Program.cs b/kerberos-auth/aspnet-server/Program.cs new file mode 100644 index 0000000..fbc70e1 --- /dev/null +++ b/kerberos-auth/aspnet-server/Program.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authentication.Negotiate; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System; + +var builder = WebApplication.CreateBuilder(args); + +// Configure Kerberos/Negotiate authentication +builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) + .AddNegotiate(); + +builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); +}); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", (HttpContext context) => +{ + var user = context.User.Identity; + if (user?.IsAuthenticated == true) + { + Console.WriteLine($"[SERVER] Authenticated user: {user.Name}"); + return Results.Ok(new + { + authenticated = true, + username = user.Name, + authenticationType = user.AuthenticationType, + message = "Kerberos authentication successful" + }); + } + else + { + Console.WriteLine("[SERVER] User not authenticated"); + return Results.Unauthorized(); + } +}); + +// Read configuration from environment +var port = Environment.GetEnvironmentVariable("SERVER_PORT") ?? "5000"; +var keytabPath = Environment.GetEnvironmentVariable("KRB5_KTNAME"); + +if (string.IsNullOrEmpty(keytabPath)) +{ + Console.WriteLine("error: KRB5_KTNAME environment variable not set"); + Environment.Exit(1); +} + +Console.WriteLine($"[SERVER] Starting on port {port}"); +Console.WriteLine($"[SERVER] Using keytab: {keytabPath}"); + +app.Urls.Add($"http://localhost:{port}"); +app.Run(); diff --git a/kerberos-auth/aspnet-server/aspnet-server.csproj b/kerberos-auth/aspnet-server/aspnet-server.csproj new file mode 100644 index 0000000..50f472d --- /dev/null +++ b/kerberos-auth/aspnet-server/aspnet-server.csproj @@ -0,0 +1,14 @@ + + + + Exe + $(TestTargetFramework) + + + + + + + + + diff --git a/kerberos-auth/client/Program.cs b/kerberos-auth/client/Program.cs new file mode 100644 index 0000000..ee9f15e --- /dev/null +++ b/kerberos-auth/client/Program.cs @@ -0,0 +1,64 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text.Json; + +// Read configuration from environment +var serverUrl = Environment.GetEnvironmentVariable("SERVER_URL"); +if (string.IsNullOrEmpty(serverUrl)) +{ + Console.WriteLine("error: SERVER_URL environment variable not set"); + Environment.Exit(1); + return; +} + +Console.WriteLine($"[CLIENT] Connecting to: {serverUrl}"); + +// Create HttpClient with default credentials (uses Kerberos) +var handler = new HttpClientHandler +{ + UseDefaultCredentials = true, + Credentials = CredentialCache.DefaultCredentials +}; + +using var client = new HttpClient(handler); + +try +{ + var response = await client.GetAsync(serverUrl); + + Console.WriteLine($"[CLIENT] Response status: {response.StatusCode}"); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"[CLIENT] Response: {content}"); + + // Parse JSON to verify authentication + var jsonDoc = JsonDocument.Parse(content); + if (jsonDoc.RootElement.TryGetProperty("authenticated", out var authProp) && + authProp.GetBoolean()) + { + Console.WriteLine("[CLIENT] SUCCESS: Kerberos authentication verified"); + Environment.Exit(0); + } + else + { + Console.WriteLine("[CLIENT] FAIL: Response does not indicate authentication"); + Environment.Exit(1); + } + } + else + { + Console.WriteLine($"[CLIENT] FAIL: HTTP {response.StatusCode}"); + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"[CLIENT] Response body: {content}"); + Environment.Exit(1); + } +} +catch (Exception ex) +{ + Console.WriteLine($"[CLIENT] error: {ex.Message}"); + Console.WriteLine($"[CLIENT] Stack trace: {ex.StackTrace}"); + Environment.Exit(1); +} diff --git a/kerberos-auth/client/client.csproj b/kerberos-auth/client/client.csproj new file mode 100644 index 0000000..4227b29 --- /dev/null +++ b/kerberos-auth/client/client.csproj @@ -0,0 +1,8 @@ + + + + Exe + $(TestTargetFramework) + + + diff --git a/kerberos-auth/setup-kdc.sh b/kerberos-auth/setup-kdc.sh new file mode 100755 index 0000000..44d1e09 --- /dev/null +++ b/kerberos-auth/setup-kdc.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +# Setup a local MIT Kerberos KDC for testing +# This script creates an ephemeral, isolated Kerberos environment + +set -euo pipefail + +# Check for required Kerberos tools +if ! command -v kdb5_util &> /dev/null; then + echo "error: kdb5_util not found. Please install Kerberos server packages." + exit 1 +fi + +if ! command -v krb5kdc &> /dev/null; then + echo "error: krb5kdc not found. Please install Kerberos server packages." + exit 1 +fi + +if ! command -v kadmin.local &> /dev/null; then + echo "error: kadmin.local not found. Please install Kerberos admin packages." + exit 1 +fi + +# Environment setup +export TEST_DIR="${1:-}" +if [[ -z "${TEST_DIR}" ]]; then + echo "error: TEST_DIR not provided" + exit 1 +fi + +export REALM="${2:-}" +if [[ -z "${REALM}" ]]; then + echo "error: REALM not provided" + exit 1 +fi + +export KDC_PORT="${3:-}" +if [[ -z "${KDC_PORT}" ]]; then + echo "error: KDC_PORT not provided" + exit 1 +fi + +export KRB5_CONFIG="${TEST_DIR}/krb5.conf" +export KRB5_KDC_PROFILE="${TEST_DIR}/kdc.conf" + +echo "Setting up KDC in ${TEST_DIR}" +echo "Realm: ${REALM}" +echo "KDC Port: ${KDC_PORT}" + +# Create krb5.conf +cat > "${KRB5_CONFIG}" << EOF +[libdefaults] + default_realm = ${REALM} + dns_lookup_realm = false + dns_lookup_kdc = false + ticket_lifetime = 24h + renew_lifetime = 7d + forwardable = true + rdns = false + default_ccache_name = FILE:${TEST_DIR}/krb5cc + +[realms] + ${REALM} = { + kdc = localhost:${KDC_PORT} + admin_server = localhost:${KDC_PORT} + default_domain = localhost + } + +[domain_realm] + .localhost = ${REALM} + localhost = ${REALM} +EOF + +# Create kdc.conf +cat > "${KRB5_KDC_PROFILE}" << EOF +[kdcdefaults] + kdc_ports = ${KDC_PORT} + kdc_tcp_ports = ${KDC_PORT} + +[realms] + ${REALM} = { + database_name = ${TEST_DIR}/principal + admin_keytab = FILE:${TEST_DIR}/kadm5.keytab + acl_file = ${TEST_DIR}/kadm5.acl + key_stash_file = ${TEST_DIR}/.k5.${REALM} + max_life = 10h 0m 0s + max_renewable_life = 7d 0h 0m 0s + master_key_type = aes256-cts + supported_enctypes = aes256-cts:normal aes128-cts:normal + default_principal_flags = +preauth + } +EOF + +# Create ACL file for kadmin +echo "*/admin@${REALM} *" > "${TEST_DIR}/kadm5.acl" + +# Initialize KDC database +echo "Initializing KDC database..." +kdb5_util create -s -P masterpassword -r "${REALM}" -d "${TEST_DIR}/principal" 2>&1 | grep -v "^Loading" || true + +# Start KDC +echo "Starting KDC..." +krb5kdc -n & +KDC_PID=$! +echo "${KDC_PID}" > "${TEST_DIR}/kdc.pid" + +# Wait for KDC to be ready +sleep 2 + +# Verify KDC is running +if ! ps -p "${KDC_PID}" > /dev/null 2>&1; then + echo "error: KDC failed to start" + exit 1 +fi + +echo "KDC started successfully (PID: ${KDC_PID})" + +# Create principals +echo "Creating principals..." +kadmin.local -r "${REALM}" -q "addprinc -randkey HTTP/localhost@${REALM}" 2>&1 | grep -v "^Authenticating" || true +kadmin.local -r "${REALM}" -q "addprinc -pw clientpassword testclient@${REALM}" 2>&1 | grep -v "^Authenticating" || true + +# Export keytabs +echo "Exporting keytabs..." +kadmin.local -r "${REALM}" -q "ktadd -k ${TEST_DIR}/http.keytab HTTP/localhost@${REALM}" 2>&1 | grep -v "^Authenticating" || true +kadmin.local -r "${REALM}" -q "ktadd -k ${TEST_DIR}/client.keytab testclient@${REALM}" 2>&1 | grep -v "^Authenticating" || true + +# Set proper permissions +chmod 600 "${TEST_DIR}"/*.keytab + +echo "KDC setup complete!" +echo "KRB5_CONFIG=${KRB5_CONFIG}" +echo "Service keytab: ${TEST_DIR}/http.keytab" +echo "Client keytab: ${TEST_DIR}/client.keytab" diff --git a/kerberos-auth/test.json b/kerberos-auth/test.json new file mode 100644 index 0000000..0d47a73 --- /dev/null +++ b/kerberos-auth/test.json @@ -0,0 +1,14 @@ +{ + "name": "kerberos-auth", + "enabled": true, + "requiresSdk": true, + "version": "8.0", + "versionSpecific": false, + "type": "bash", + "cleanup": true, + "skipWhen": [ + "ubi8-repos", // ubi repos don't contain kerberos utils + "ubi9-repos", // ubi repos don't contain kerberos utils + "ubi10-repos" // ubi repos don't contain kerberos utils + ] +} diff --git a/kerberos-auth/test.sh b/kerberos-auth/test.sh new file mode 100755 index 0000000..f09e4f0 --- /dev/null +++ b/kerberos-auth/test.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash + +set -euo pipefail +IFS=$'\n\t' + +# Enable verbose output for debugging +set -x + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEST_DIR="" +KDC_PID="" +ASPNET_SERVER_PID="" + +# Cleanup function +cleanup() { + local exit_code=$? + set +e + + echo "Cleaning up..." + + # Kill server + if [[ -n "${ASPNET_SERVER_PID}" ]] && ps -p "${ASPNET_SERVER_PID}" > /dev/null 2>&1; then + echo "Stopping ASP.NET Core server (PID: ${ASPNET_SERVER_PID})" + kill "${ASPNET_SERVER_PID}" 2>/dev/null || true + sleep 1 + kill -9 "${ASPNET_SERVER_PID}" 2>/dev/null || true + fi + + # Kill KDC + if [[ -n "${KDC_PID}" ]] && ps -p "${KDC_PID}" > /dev/null 2>&1; then + echo "Stopping KDC (PID: ${KDC_PID})" + kill "${KDC_PID}" 2>/dev/null || true + sleep 1 + kill -9 "${KDC_PID}" 2>/dev/null || true + elif [[ -n "${TEST_DIR}" ]] && [[ -f "${TEST_DIR}/kdc.pid" ]]; then + KDC_PID=$(cat "${TEST_DIR}/kdc.pid") + if ps -p "${KDC_PID}" > /dev/null 2>&1; then + echo "Stopping KDC from pidfile (PID: ${KDC_PID})" + kill "${KDC_PID}" 2>/dev/null || true + sleep 1 + kill -9 "${KDC_PID}" 2>/dev/null || true + fi + fi + + # Clean up temp directory + if [[ -n "${TEST_DIR}" ]] && [[ -d "${TEST_DIR}" ]]; then + echo "Removing temporary directory: ${TEST_DIR}" + rm -rf "${TEST_DIR}" + fi + + if [[ ${exit_code} -ne 0 ]]; then + echo "Test FAILED with exit code ${exit_code}" + fi + + exit ${exit_code} +} + +trap cleanup EXIT INT TERM + +# Generate random values for isolation +RANDOM_ID="${RANDOM}${RANDOM}" +REALM="TEST${RANDOM_ID}.LOCAL" +KDC_PORT=$((10000 + (RANDOM % 20000))) +ASPNET_PORT=$((30000 + (RANDOM % 20000))) + +# Ensure ports are different +while [[ ${ASPNET_PORT} -eq ${KDC_PORT} ]]; do + ASPNET_PORT=$((30000 + (RANDOM % 20000))) +done + +echo "==========================================" +echo "Kerberos Authentication Test" +echo "==========================================" +echo "Realm: ${REALM}" +echo "KDC Port: ${KDC_PORT}" +echo "ASP.NET Core Port: ${ASPNET_PORT}" +echo "==========================================" + +# Create temporary directory +TEST_DIR=$(mktemp -d -t kerberos-test-XXXXXX) +echo "Test directory: ${TEST_DIR}" + +# Setup KDC +echo "" +echo "Setting up Kerberos KDC..." +export KRB5_CONFIG="${TEST_DIR}/krb5.conf" +export KRB5_KDC_PROFILE="${TEST_DIR}/kdc.conf" +export KRB5CCNAME="FILE:${TEST_DIR}/krb5cc" + +bash "${SCRIPT_DIR}/setup-kdc.sh" "${TEST_DIR}" "${REALM}" "${KDC_PORT}" + +# Read KDC PID +if [[ -f "${TEST_DIR}/kdc.pid" ]]; then + KDC_PID=$(cat "${TEST_DIR}/kdc.pid") + echo "KDC running with PID: ${KDC_PID}" +fi + +# Acquire client credentials +echo "" +echo "Acquiring client Kerberos credentials..." +export KRB5_KTNAME="${TEST_DIR}/client.keytab" +kinit -kt "${KRB5_KTNAME}" "testclient@${REALM}" + +# Verify ticket +echo "" +echo "Verifying Kerberos ticket..." +klist + +echo "" +echo "==========================================" +echo "ASP.NET Core with Kerberos" +echo "==========================================" + +# Build client +echo "Building client..." +pushd "${SCRIPT_DIR}/client" +dotnet build -c Release +popd + +# Build ASP.NET Core server +echo "Building ASP.NET Core server..." +pushd "${SCRIPT_DIR}/aspnet-server" +dotnet build -c Release +popd + +# Start ASP.NET Core server +echo "Starting ASP.NET Core server on port ${ASPNET_PORT}..." +export KRB5_KTNAME="${TEST_DIR}/http.keytab" +export SERVER_PORT="${ASPNET_PORT}" + +dotnet "${SCRIPT_DIR}"/aspnet-server/bin/Release/*/aspnet-server.dll & +ASPNET_SERVER_PID=$! +echo "ASP.NET Core server started (PID: ${ASPNET_SERVER_PID})" + +# Wait for server to be ready +../run-until-success-with-backoff curl "http://localhost:${ASPNET_PORT}/" --negotiate -u : || { + echo "FAIL: ASP.NET Core server did not start properly" + exit 1 +} + +# Run client test +echo "Running client test..." +export KRB5_KTNAME="${TEST_DIR}/client.keytab" +export SERVER_URL="http://localhost:${ASPNET_PORT}/" + +if dotnet "${SCRIPT_DIR}"/client/bin/Release/*/client.dll; then + echo "" + echo "======================================" + echo "ASP.NET Core Test: PASS" + echo "======================================" +else + echo "" + echo "======================================" + echo "ASP.NET Core Test: FAIL" + echo "======================================" + exit 1 +fi + +# Stop ASP.NET Core server +echo "Stopping ASP.NET Core server..." +kill "${ASPNET_SERVER_PID}" 2>/dev/null || true +sleep 1 +kill -9 "${ASPNET_SERVER_PID}" 2>/dev/null || true +ASPNET_SERVER_PID="" + +echo "" +echo "==========================================" +echo "Kerberos Authentication Test: PASS" +echo "==========================================" + +exit 0