using System.Linq; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using FrameworkDAL.CustomCode.CommonReport; using FrameworkDAL.DTO.KeyCloak; using GB5Shared.ExcelExport; using GB5Shared.Export.CSVExport; using GB5Shared.GB5CommonFunction; using GB5Shared.Vault; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace FrameworkDAL.CustomCode.KeyCloak { public class KeyCloakDAL : IKeyCloakDAL { private readonly IConfiguration _configuration; private readonly IVaultService _vaultService; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; // Vault paths for the Keycloak admin-API credential (admin-cli password grant against // /realms/master) — never read from appsettings.json. See GB5Shared/Vault/IVaultService. private const string AdminUsernameVaultPath = "keycloak/admin-username"; private const string AdminPasswordVaultPath = "keycloak/admin-password"; // "oidc" is the shared, already-established named HttpClient for internal/self-signed-CA // OIDC endpoints (see FrameworkSL/Program.cs's HttpClient Registrations region, and // FrameworkSL/Endpoints/SSO/SSOLoginCallback.cs, which already uses the same client for // the real Authorization Code flow) — reused here instead of each method building its own // ad hoc HttpClient/HttpClientHandler with a inline TLS-bypass callback. private const string OidcHttpClientName = "oidc"; public KeyCloakDAL(IConfiguration configuration, IVaultService vaultService, IHttpClientFactory httpClientFactory, ILogger logger) { _configuration = configuration; _vaultService = vaultService; _httpClientFactory = httpClientFactory; _logger = logger; } public async Task GetMasterAccessToken() { try { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); string username = await _vaultService.GetSecretAsync(AdminUsernameVaultPath); string password = await _vaultService.GetSecretAsync(AdminPasswordVaultPath); var formData = new FormUrlEncodedContent(new[] { new KeyValuePair("client_id", "admin-cli"), new KeyValuePair("username", username), new KeyValuePair("password", password), new KeyValuePair("grant_type", "password") }); var keycloakConfig = _configuration.GetSection("Keycloak"); string keycloakHost = keycloakConfig.GetValue("KeycloakHost")!; var response = await httpClient.PostAsync( $"{keycloakHost}/realms/master/protocol/openid-connect/token", formData); response.EnsureSuccessStatusCode(); var jsonString = await response.Content.ReadAsStringAsync(); using var jsonDoc = JsonDocument.Parse(jsonString); var accessToken = jsonDoc.RootElement.GetProperty("access_token").GetString(); return accessToken!; } catch (Exception ex) { throw new Exception("Error fetching Keycloak master access token", ex); } } public async Task CreateRealm(string RealmName) { string AccessToken = await GetMasterAccessToken(); try { var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var CreateRealmUrl = $"{KeycloakHost}/admin/realms"; var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); var realmDefinition = new { realm = RealmName, enabled = true }; var realmJson = System.Text.Json.JsonSerializer.Serialize(realmDefinition); var CreateRequest = new HttpRequestMessage(HttpMethod.Post, CreateRealmUrl) { Content = new StringContent(realmJson, Encoding.UTF8, "application/json") }; CreateRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); var createResponse = await httpClient.SendAsync(CreateRequest); var createContent = await createResponse.Content.ReadAsStringAsync(); if (createResponse.IsSuccessStatusCode) { return "Realm created successfully"; } else { return "Error creating realm: " + createContent; } } catch (Exception) { throw; } finally { AccessToken = null!; } } // Fixed 2026-09-08 — the original always hardcoded publicClient=true and never set // redirectUris/enabled from the DTO, so every client created here was unconditionally // public with no redirect URI, regardless of what the caller asked for (KeyCloakDTO has // PublicClient/RedirectUris/ClientEnabled fields; none were previously read). Now honors // them, and — critically — never returns a generated confidential-client secret over the // wire: it's written straight to Vault at "keycloak/{realm}/{clientId}-client-secret", // the exact path KeyCloakService.cs's ClientSecretVaultPath already reads from, so a // client created here is immediately usable by that login flow with zero extra wiring. public async Task CreateClient(KeyCloakDTO KeyCloakDTO) { string AccessToken = await GetMasterAccessToken(); try { var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var CreateClientUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/clients"; var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); bool isPublic = !string.Equals(KeyCloakDTO.PublicClient, "false", StringComparison.OrdinalIgnoreCase); bool enabled = !string.Equals(KeyCloakDTO.ClientEnabled, "false", StringComparison.OrdinalIgnoreCase); string[] redirectUris = string.IsNullOrWhiteSpace(KeyCloakDTO.RedirectUris) ? Array.Empty() : KeyCloakDTO.RedirectUris.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var clientDefinition = new { clientId = KeyCloakDTO.ClientId, name = KeyCloakDTO.ClientName, enabled, publicClient = isPublic, standardFlowEnabled = true, directAccessGrantsEnabled = false, redirectUris, webOrigins = redirectUris.Select(u => u.TrimEnd('*', '/')).ToArray(), protocol = "openid-connect" }; var clientJson = System.Text.Json.JsonSerializer.Serialize(clientDefinition); var CreateRequest = new HttpRequestMessage(HttpMethod.Post, CreateClientUrl) { Content = new StringContent(clientJson, Encoding.UTF8, "application/json") }; CreateRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); var createResponse = await httpClient.SendAsync(CreateRequest); var createContent = await createResponse.Content.ReadAsStringAsync(); if (!createResponse.IsSuccessStatusCode) { return "Error creating client: " + createContent; } if (!isPublic) { // Confidential client — Keycloak auto-generates a secret; fetch it and store // it server-side only. Never included in this method's return value. string getClientJson = await GetClientByClientIdAsync(KeyCloakDTO.RealmName, KeyCloakDTO.ClientId); using var doc = JsonDocument.Parse(getClientJson); if (doc.RootElement.ValueKind == JsonValueKind.Array && doc.RootElement.GetArrayLength() > 0 && doc.RootElement[0].TryGetProperty("secret", out var secretEl)) { string vaultPath = $"keycloak/{KeyCloakDTO.RealmName}/{KeyCloakDTO.ClientId}-client-secret"; await _vaultService.SetSecretAsync(vaultPath, secretEl.GetString() ?? ""); _logger.LogInformation( "KeyCloakDAL.CreateClient: confidential client '{ClientId}' created in realm '{Realm}', secret stored at Vault path '{VaultPath}'", KeyCloakDTO.ClientId, KeyCloakDTO.RealmName, vaultPath); } else { _logger.LogWarning( "KeyCloakDAL.CreateClient: client '{ClientId}' created in realm '{Realm}' but no secret could be read back to store in Vault", KeyCloakDTO.ClientId, KeyCloakDTO.RealmName); } } return "Client created successfully"; } catch (Exception) { throw; } finally { AccessToken = null!; } } public async Task CreateRole(KeyCloakDTO KeyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { string AccessToken = await GetMasterAccessToken(); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var createRoleUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/roles"; var roleDefinition = new { name = KeyCloakDTO.RoleName, description = KeyCloakDTO.Description }; var roleJson = System.Text.Json.JsonSerializer.Serialize(roleDefinition); var createRequest = new HttpRequestMessage(HttpMethod.Post, createRoleUrl) { Content = new StringContent(roleJson, Encoding.UTF8, "application/json") }; createRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); var createResponse = await httpClient.SendAsync(createRequest); var createContent = await createResponse.Content.ReadAsStringAsync(); if (createResponse.IsSuccessStatusCode) { return "Role created successfully"; } else { return "Error creating role: " + createContent; } } catch (Exception) { throw; } } public async Task CreateUser(KeyCloakDTO keyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { // Validate input early if (keyCloakDTO == null) return "Invalid request: keyCloakDTO is null."; if (string.IsNullOrWhiteSpace(keyCloakDTO.RealmName)) return "Invalid request: RealmName is required."; if (string.IsNullOrWhiteSpace(keyCloakDTO.UserName)) return "Invalid request: UserName is required."; // 1) Get admin token string accessToken = await GetMasterAccessToken(); if (string.IsNullOrWhiteSpace(accessToken)) return "Error: could not obtain access token."; var keycloakConfig = _configuration.GetSection("Keycloak"); string keycloakHost = keycloakConfig.GetValue("KeycloakHost")!; if (string.IsNullOrWhiteSpace(keycloakHost)) return "KeycloakHost not configured."; // 2) Create user var createUserUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users"; var userDefinition = new { username = keyCloakDTO.UserName, enabled = true, firstName = keyCloakDTO.FirstName, lastName = keyCloakDTO.LastName, email = keyCloakDTO.EMail }; var userJson = System.Text.Json.JsonSerializer.Serialize(userDefinition); var createUserRequest = new HttpRequestMessage(HttpMethod.Post, createUserUrl) { Content = new StringContent(userJson, Encoding.UTF8, "application/json") }; createUserRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var createUserResponse = await httpClient.SendAsync(createUserRequest); var createUserContent = await createUserResponse.Content.ReadAsStringAsync(); if (!createUserResponse.IsSuccessStatusCode && !createUserContent.Contains("User exists", StringComparison.OrdinalIgnoreCase)) { return $"Error creating user: {createUserContent}"; } // 3) Retry to get user ID (Keycloak may need time to index) string? userId = null; const int maxAttempts = 8; const int delayMs = 2000; for (int attempt = 1; attempt <= maxAttempts; attempt++) { await Task.Delay(delayMs); var getUsersUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users?username={Uri.EscapeDataString(keyCloakDTO.UserName)}"; var getUsersRequest = new HttpRequestMessage(HttpMethod.Get, getUsersUrl); getUsersRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var getUsersResponse = await httpClient.SendAsync(getUsersRequest); var getUsersContent = await getUsersResponse.Content.ReadAsStringAsync(); if (getUsersResponse.IsSuccessStatusCode) { using var usersJson = JsonDocument.Parse(getUsersContent); if (usersJson.RootElement.GetArrayLength() > 0) { userId = usersJson.RootElement[0].GetProperty("id").GetString(); break; } } // optional debug _logger.LogDebug("KeyCloak CreateUser attempt {Attempt}: user not indexed yet", attempt); } if (string.IsNullOrEmpty(userId)) { return "User created successfully, but Keycloak has not indexed it yet. Try again after a few seconds."; } // 4) Validate password present before attempting to set if (string.IsNullOrWhiteSpace(keyCloakDTO.PassWord)) { return $"User created (ID: {userId}) but password not provided — please pass a valid password in keyCloakDTO.PassWord."; } // 5) Set password (non-temporary) var resetPasswordUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users/{userId}/reset-password"; var passwordDefinition = new { type = "password", temporary = false, value = keyCloakDTO.PassWord.Trim() }; var passwordJson = System.Text.Json.JsonSerializer.Serialize(passwordDefinition); var resetPasswordRequest = new HttpRequestMessage(HttpMethod.Put, resetPasswordUrl) { Content = new StringContent(passwordJson, Encoding.UTF8, "application/json") }; resetPasswordRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var resetPasswordResponse = await httpClient.SendAsync(resetPasswordRequest); var resetPasswordContent = await resetPasswordResponse.Content.ReadAsStringAsync(); if (!resetPasswordResponse.IsSuccessStatusCode) { return $"User created (ID: {userId}) but password set failed: {resetPasswordContent}"; } return $"User created successfully (User ID: {userId}) and password set."; } catch (Exception ex) { // Return helpful error instead of masking the exception return $"Error in CreateUser: {ex.Message} | Inner: {ex.InnerException?.Message}"; } } public async Task ForgotPassword(KeyCloakDTO keyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { if (keyCloakDTO == null) return "Invalid request: keyCloakDTO is null."; if (string.IsNullOrWhiteSpace(keyCloakDTO.RealmName)) return "Invalid request: RealmName is required."; if (string.IsNullOrWhiteSpace(keyCloakDTO.UserName)) return "Invalid request: UserName is required."; string accessToken = await GetMasterAccessToken(); if (string.IsNullOrWhiteSpace(accessToken)) return "Error: could not obtain access token."; var keycloakConfig = _configuration.GetSection("Keycloak"); string keycloakHost = keycloakConfig.GetValue("KeycloakHost")!; if (string.IsNullOrWhiteSpace(keycloakHost)) return "KeycloakHost not configured."; // 1) Get user by username var getUserUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users?username={Uri.EscapeDataString(keyCloakDTO.UserName)}"; var getUserRequest = new HttpRequestMessage(HttpMethod.Get, getUserUrl); getUserRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var getUserResponse = await httpClient.SendAsync(getUserRequest); var getUserContent = await getUserResponse.Content.ReadAsStringAsync(); if (!getUserResponse.IsSuccessStatusCode) return $"Error fetching user: {getUserContent}"; using var usersJson = JsonDocument.Parse(getUserContent); if (usersJson.RootElement.GetArrayLength() == 0) return $"User '{keyCloakDTO.UserName}' not found in realm '{keyCloakDTO.RealmName}'."; string userId = usersJson.RootElement[0].GetProperty("id").GetString()!; // 2) Trigger reset password email var executeActionsUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users/{userId}/execute-actions-email"; var actions = new[] { "UPDATE_PASSWORD" }; var actionsJson = System.Text.Json.JsonSerializer.Serialize(actions); var executeRequest = new HttpRequestMessage(HttpMethod.Put, executeActionsUrl) { Content = new StringContent(actionsJson, Encoding.UTF8, "application/json") }; executeRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var executeResponse = await httpClient.SendAsync(executeRequest); var executeContent = await executeResponse.Content.ReadAsStringAsync(); if (!executeResponse.IsSuccessStatusCode) return $"Failed to send reset email: {executeContent}"; return $"Password reset email sent successfully to user '{keyCloakDTO.UserName}'."; } catch (Exception ex) { return $"Error in ForgotPassword: {ex.Message} | Inner: {ex.InnerException?.Message}"; } } public async Task SetUserPasswordByUsername(KeyCloakDTO keyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { string accessToken = await GetMasterAccessToken(); var keycloakConfig = _configuration.GetSection("Keycloak"); string keycloakHost = keycloakConfig.GetValue("KeycloakHost")!; // 1) Get the User ID by username var getUsersUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users?username={keyCloakDTO.UserName}"; var getUsersRequest = new HttpRequestMessage(HttpMethod.Get, getUsersUrl); getUsersRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var getUsersResponse = await httpClient.SendAsync(getUsersRequest); var getUsersContent = await getUsersResponse.Content.ReadAsStringAsync(); if (!getUsersResponse.IsSuccessStatusCode) { return $"Error retrieving user: {getUsersResponse.StatusCode} - {getUsersContent}"; } using var usersJson = JsonDocument.Parse(getUsersContent); if (usersJson.RootElement.GetArrayLength() == 0) { return $"Error: User '{keyCloakDTO.UserName}' not found in realm '{keyCloakDTO.RealmName}'."; } var userId = usersJson.RootElement[0].GetProperty("id").GetString(); // 2) Set Password var resetPasswordUrl = $"{keycloakHost}/admin/realms/{keyCloakDTO.RealmName}/users/{userId}/reset-password"; var passwordDefinition = new { type = "password", temporary = false, value = keyCloakDTO.PassWord }; var passwordJson = System.Text.Json.JsonSerializer.Serialize(passwordDefinition); var resetPasswordRequest = new HttpRequestMessage(HttpMethod.Put, resetPasswordUrl) { Content = new StringContent(passwordJson, Encoding.UTF8, "application/json") }; resetPasswordRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var resetPasswordResponse = await httpClient.SendAsync(resetPasswordRequest); var resetPasswordContent = await resetPasswordResponse.Content.ReadAsStringAsync(); if (resetPasswordResponse.IsSuccessStatusCode) { return $"Password set successfully for user '{keyCloakDTO.UserName}'."; } else { return $"Error setting password: {resetPasswordResponse.StatusCode} - {resetPasswordContent}"; } } catch (Exception ex) { throw new Exception("Error in SetUserPasswordByUsername", ex); } } public async Task GetUserByUsername(KeyCloakDTO KeyCloakDTO) { string accessToken = await GetMasterAccessToken(); try { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; // 2. Get user var getUserUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/users?username={KeyCloakDTO.UserName}"; var request = new HttpRequestMessage(HttpMethod.Get, getUserUrl); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await httpClient.SendAsync(request); var responseContent = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { return "Error querying user: " + responseContent; } var json = JsonDocument.Parse(responseContent); if (json.RootElement.GetArrayLength() == 0) { return "User not found. Please contact administrator."; } else { // User found var user = json.RootElement[0]; string result = System.Text.Json.JsonSerializer.Serialize(user); return result; } } catch (Exception) { throw; } } public async Task KeyCloakAuthenticateUser(KeyCloakDTO KeyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { // 1. Get Admin token (to call admin APIs) string adminToken = await GetMasterAccessToken(); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; // 2. Check if user exists var getUserUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/users?username={KeyCloakDTO.UserName}"; var getUserRequest = new HttpRequestMessage(HttpMethod.Get, getUserUrl); getUserRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getUserResponse = await httpClient.SendAsync(getUserRequest); var getUserContent = await getUserResponse.Content.ReadAsStringAsync(); if (!getUserResponse.IsSuccessStatusCode) { return $"Error verifying user existence: {getUserContent}"; } var usersJson = JsonDocument.Parse(getUserContent); if (usersJson.RootElement.GetArrayLength() == 0) { return $"User '{KeyCloakDTO.UserName}' not found"; } var userId = usersJson.RootElement[0].GetProperty("id").GetString(); // 3. Authenticate username/password var authUrl = $"{KeycloakHost}/realms/{KeyCloakDTO.RealmName}/protocol/openid-connect/token"; var authParams = new Dictionary { {"grant_type", "password"}, {"client_id",KeyCloakDTO.ClientId}, {"username", KeyCloakDTO.UserName}, {"password", KeyCloakDTO.PassWord} }; var authRequest = new HttpRequestMessage(HttpMethod.Post, authUrl) { Content = new FormUrlEncodedContent(authParams) }; var authResponse = await httpClient.SendAsync(authRequest); var authContent = await authResponse.Content.ReadAsStringAsync(); if (!authResponse.IsSuccessStatusCode) { return $"Authentication failed for user '{KeyCloakDTO.UserName}'. Invalid credentials or client."; } // 4. Verify user is mapped to client // First, get the client UUID var getClientUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/clients?clientId={KeyCloakDTO.ClientId}"; var getClientRequest = new HttpRequestMessage(HttpMethod.Get, getClientUrl); getClientRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getClientResponse = await httpClient.SendAsync(getClientRequest); var getClientContent = await getClientResponse.Content.ReadAsStringAsync(); if (!getClientResponse.IsSuccessStatusCode) { return $"Error retrieving client '{KeyCloakDTO.ClientId}': {getClientContent}"; } var clientsJson = JsonDocument.Parse(getClientContent); if (clientsJson.RootElement.GetArrayLength() == 0) { return $"Client '{KeyCloakDTO.ClientId}' not found in realm '{KeyCloakDTO.RealmName}'."; } var clientUuid = clientsJson.RootElement[0].GetProperty("id").GetString(); // 5. Get role mappings for this user in this client var getRolesUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/users/{userId}/role-mappings/clients/{clientUuid}"; var getRolesRequest = new HttpRequestMessage(HttpMethod.Get, getRolesUrl); getRolesRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getRolesResponse = await httpClient.SendAsync(getRolesRequest); var getRolesContent = await getRolesResponse.Content.ReadAsStringAsync(); if (!getRolesResponse.IsSuccessStatusCode) { return $"Error retrieving role mappings: {getRolesContent}"; } var rolesJson = JsonDocument.Parse(getRolesContent); if (rolesJson.RootElement.GetArrayLength() == 0) { return $"User '{KeyCloakDTO.UserName}' has no roles in client '{KeyCloakDTO.ClientId}'."; } // All checks passed return "Authentication successful."; } catch (Exception ex) { throw new Exception("Error in AuthenticateUser", ex); } } public async Task CreateClientRole(KeyCloakDTO KeyCloakDTO) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { // 1. Get admin token string adminToken = await GetMasterAccessToken(); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; // 2. Get client UUID var getClientUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/clients?clientId={KeyCloakDTO.ClientId}"; var getClientRequest = new HttpRequestMessage(HttpMethod.Get, getClientUrl); getClientRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getClientResponse = await httpClient.SendAsync(getClientRequest); var getClientContent = await getClientResponse.Content.ReadAsStringAsync(); if (!getClientResponse.IsSuccessStatusCode) { return $"Error retrieving client '{KeyCloakDTO.ClientId}': {getClientContent}"; } var clientsJson = JsonDocument.Parse(getClientContent); if (clientsJson.RootElement.GetArrayLength() == 0) { return $"Client '{KeyCloakDTO.ClientId}' not found in realm '{KeyCloakDTO.RealmName}'."; } var clientUuid = clientsJson.RootElement[0].GetProperty("id").GetString(); // 3. Create the role var createRoleUrl = $"{KeycloakHost}/admin/realms/{KeyCloakDTO.RealmName}/clients/{clientUuid}/roles"; var roleDefinition = new { name = KeyCloakDTO.RoleName, description = KeyCloakDTO.Description }; var roleJson = System.Text.Json.JsonSerializer.Serialize(roleDefinition); var createRoleRequest = new HttpRequestMessage(HttpMethod.Post, createRoleUrl) { Content = new StringContent(roleJson, Encoding.UTF8, "application/json") }; createRoleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var createRoleResponse = await httpClient.SendAsync(createRoleRequest); var createRoleContent = await createRoleResponse.Content.ReadAsStringAsync(); if (createRoleResponse.IsSuccessStatusCode) { return $"Role '{KeyCloakDTO.RealmName}' created successfully under client '{KeyCloakDTO.ClientId}'."; } else if (createRoleResponse.StatusCode == System.Net.HttpStatusCode.Conflict) { return $"Role '{KeyCloakDTO.RoleName}' already exists under client '{KeyCloakDTO.ClientId}'."; } else { return $"Error creating role: {createRoleContent}"; } } catch (Exception ex) { throw new Exception("Error in CreateClientRole", ex); } } public async Task AssignClientRoleToUser(KeyCloakDTO dto) { var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); try { // 1. Get Admin token string adminToken = await GetMasterAccessToken(); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; // 2. Get client UUID var getClientUrl = $"{KeycloakHost}/admin/realms/{dto.RealmName}/clients?clientId={dto.ClientId}"; var getClientRequest = new HttpRequestMessage(HttpMethod.Get, getClientUrl); getClientRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getClientResponse = await httpClient.SendAsync(getClientRequest); var getClientContent = await getClientResponse.Content.ReadAsStringAsync(); if (!getClientResponse.IsSuccessStatusCode) return $"Error retrieving client: {getClientContent}"; var clientsJson = JsonDocument.Parse(getClientContent); if (clientsJson.RootElement.GetArrayLength() == 0) return $"Client '{dto.ClientId}' not found."; var clientUuid = clientsJson.RootElement[0].GetProperty("id").GetString(); // 3. Get the role representation of the role in the client var getRoleUrl = $"{KeycloakHost}/admin/realms/{dto.RealmName}/clients/{clientUuid}/roles/{dto.RoleName}"; var getRoleRequest = new HttpRequestMessage(HttpMethod.Get, getRoleUrl); getRoleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getRoleResponse = await httpClient.SendAsync(getRoleRequest); var getRoleContent = await getRoleResponse.Content.ReadAsStringAsync(); if (!getRoleResponse.IsSuccessStatusCode) return $"Error retrieving role '{dto.RoleName}': {getRoleContent}"; var roleJson = JsonDocument.Parse(getRoleContent); var roleRepresentation = new[] { new { id = roleJson.RootElement.GetProperty("id").GetString(), name = roleJson.RootElement.GetProperty("name").GetString() } }; // 4. Get the user ID by username var getUserUrl = $"{KeycloakHost}/admin/realms/{dto.RealmName}/users?username={dto.UserName}"; var getUserRequest = new HttpRequestMessage(HttpMethod.Get, getUserUrl); getUserRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var getUserResponse = await httpClient.SendAsync(getUserRequest); var getUserContent = await getUserResponse.Content.ReadAsStringAsync(); if (!getUserResponse.IsSuccessStatusCode) return $"Error retrieving user '{dto.UserName}': {getUserContent}"; var usersJson = JsonDocument.Parse(getUserContent); if (usersJson.RootElement.GetArrayLength() == 0) return $"User '{dto.UserName}' not found."; var userId = usersJson.RootElement[0].GetProperty("id").GetString(); // 5. Assign the role to the user var assignRoleUrl = $"{KeycloakHost}/admin/realms/{dto.RealmName}/users/{userId}/role-mappings/clients/{clientUuid}"; var assignRoleRequest = new HttpRequestMessage(HttpMethod.Post, assignRoleUrl) { Content = new StringContent(JsonConvert.SerializeObject(roleRepresentation), Encoding.UTF8, "application/json") }; assignRoleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var assignRoleResponse = await httpClient.SendAsync(assignRoleRequest); var assignRoleContent = await assignRoleResponse.Content.ReadAsStringAsync(); if (assignRoleResponse.IsSuccessStatusCode) return $"Role '{dto.RoleName}' assigned to user '{dto.UserName}' successfully."; return $"Error assigning role: {assignRoleContent}"; } catch (Exception ex) { throw new Exception("Error in AssignClientRoleToUser", ex); } } public async Task CreateClientAsync(KeycloakClientDTO KeycloakClientDTO) { string adminToken = await GetMasterAccessToken(); try { var allRedirectUris = (KeycloakClientDTO.RedirectUris ?? new List()) .Concat(KeycloakClientDTO.PostLogoutRedirectUris ?? new List()) .Distinct() .ToArray(); var newClient = new { clientId = KeycloakClientDTO.ClientId, name = KeycloakClientDTO.Name, redirectUris = allRedirectUris, baseUrl = KeycloakClientDTO.BaseUrl, adminUrl = KeycloakClientDTO.AdminUrl, rootUrl = KeycloakClientDTO.RootUrl, webOrigins = KeycloakClientDTO.WebOrigins, publicClient = false, directAccessGrantsEnabled = true }; var client = _httpClientFactory.CreateClient(OidcHttpClientName); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var requestUrl = $"{KeycloakHost}/admin/realms/{KeycloakClientDTO.Realm}/clients"; var content = new StringContent(JsonConvert.SerializeObject(newClient), Encoding.UTF8, "application/json"); var response = await client.PostAsync(requestUrl, content); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { return "Client created successfully."; } return $"Error: {responseContent}"; } catch (Exception ex) { return $"Exception: {ex.Message}"; } } public async Task GetClientsByRealmAsync(string realmName) { string adminToken = await GetMasterAccessToken(); try { var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var client = _httpClientFactory.CreateClient(OidcHttpClientName); // Endpoint: GET /admin/realms/{realm}/clients string endpoint = $"{KeycloakHost}/admin/realms/{realmName}/clients"; var request = new HttpRequestMessage(HttpMethod.Get, endpoint); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); HttpResponseMessage response = await client.SendAsync(request); string content = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { return content; // This will be a JSON array of client representations } else { return $"Error fetching clients: {content}"; } } catch (Exception ex) { return $"Exception: {ex.Message}"; } } public async Task GetClientByClientIdAsync(string realm, string clientId) { string adminToken = await GetMasterAccessToken(); try { var client = _httpClientFactory.CreateClient(OidcHttpClientName); var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var url = $"{KeycloakHost}/admin/realms/{realm}/clients?clientId={clientId}"; var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var response = await client.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) return content; else return "Error: " + content; } catch (Exception ex) { throw new Exception("Error retrieving client", ex); } } public async Task UpdateClientAsync(string realm, string clientId, KeycloakClientDTO updatedClientDTO) { string adminToken = await GetMasterAccessToken(); try { // First get the client UUID string getClientJson = await GetClientByClientIdAsync(realm, clientId); var clients = JsonDocument.Parse(getClientJson); if (clients.RootElement.GetArrayLength() == 0) return $"Client '{clientId}' not found."; var clientUuid = clients.RootElement[0].GetProperty("id").GetString(); var updatedClient = new { id = clientUuid, clientId = updatedClientDTO.ClientId, redirectUris = updatedClientDTO.RedirectUris, baseUrl = updatedClientDTO.BaseUrl, adminUrl = updatedClientDTO.AdminUrl, enabled = true }; var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); var request = new HttpRequestMessage(HttpMethod.Put, $"{KeycloakHost}/admin/realms/{realm}/clients/{clientUuid}"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); request.Content = new StringContent(JsonConvert.SerializeObject(updatedClient), Encoding.UTF8, "application/json"); var response = await httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) return $"Client '{clientId}' updated successfully."; else return "Error updating client: " + content; } catch (Exception ex) { throw new Exception("Error updating client", ex); } } public async Task DeleteClientAsync(string realm, string clientId) { string adminToken = await GetMasterAccessToken(); try { var KeycloakConfig = _configuration.GetSection("Keycloak"); string KeycloakHost = KeycloakConfig.GetValue("KeycloakHost")!; // First get the client UUID string getClientJson = await GetClientByClientIdAsync(realm, clientId); var clients = JsonDocument.Parse(getClientJson); if (clients.RootElement.GetArrayLength() == 0) return $"Client '{clientId}' not found."; var clientUuid = clients.RootElement[0].GetProperty("id").GetString(); var httpClient = _httpClientFactory.CreateClient(OidcHttpClientName); var request = new HttpRequestMessage(HttpMethod.Delete, $"{KeycloakHost}/admin/realms/{realm}/clients/{clientUuid}"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); var response = await httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) return $"Client '{clientId}' deleted successfully."; else return "Error deleting client: " + content; } catch (Exception ex) { throw new Exception("Error deleting client", ex); } } } }