Reputation: 1100
I've got a Keycloak instance setup as a local docker container, where I don't want users to use the Keycloak UI to register themselves, instead I require the users to use an ASP.NET (6) WebApi endpoint that can be used to register users in Keycloak.
I've created a client that has currently got the service account role: 'manage-users'.
In my Api project I've exposed an endpoint 'api/register' that would make a HTTP POST request to '{keycloakUrl}/admin/realms/{realm}/users' that allows me to create a user.
How do I go about assigning a role 'customer' to the users registered via the 'api/register' endpoint?
Furthermore; how do I limit the clients permission to only be able to create an account instead of having all the management permissions set by the 'manage-users' role?
Upvotes: 0
Views: 1252
Reputation: 19
I think you can create a group for your Keycloak client and map the role that performs ONLY the desired action, and then add the users who need only that permission to it. Since you want to do this via request, first add the user to that group using something like:
private async Task<string> GetGroupIdByNameAsync(string groupName)
{
var tokenResponse = await GetAdminTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var groupUrl = $"http://localhost:8080/admin/realms/keycloacktest/groups?search={groupName}";
var response = await _httpClient.GetAsync(groupUrl);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Failed to retrieve groups: {response.StatusCode}, {error}");
}
var jsonResponse = await response.Content.ReadAsStringAsync();
var groups = JsonConvert.DeserializeObject<List<GroupResponseDTO>>(jsonResponse);
var group = groups?.FirstOrDefault(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase));
return group?.Id ?? throw new NotFoundException("Group not found");
}
private async Task AddUserToGroupAsync(string userId, string groupId)
{
var tokenResponse = await GetAdminTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var addGroupUrl = $"http://localhost:8080/admin/realms/keycloacktest/users/{userId}/groups/{groupId}";
var response = await _httpClient.PutAsync(addGroupUrl, null);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Failed to add user to group: {response.StatusCode}, {error}");
}
}
After obtaining the group ID and adding the new user to it, do something like this:
private async Task<List<RoleMappingDTO>> GetRolesByGroupIdAsync(string groupId)
{
var tokenResponse = await GetAdminTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var roleUrl = $"http://localhost:8080/admin/realms/keycloacktest/groups/{groupId}/role-mappings";
var response = await _httpClient.GetAsync(roleUrl);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Failed to retrieve roles for group {groupId}: {response.StatusCode}, {error}");
}
var jsonResponse = await response.Content.ReadAsStringAsync();
var clientMappingsResponse = JsonConvert.DeserializeObject<ClientMappingsResponseDTO>(jsonResponse);
if (clientMappingsResponse?.ClientMappings == null || !clientMappingsResponse.ClientMappings.ContainsKey("keycloak-client"))
{
throw new Exception($"Client keycloak-client not found or mappings empty.");
}
var mappings = clientMappingsResponse.ClientMappings["keycloak-client"].Mappings;
return mappings;
}
private async Task AddRoleToUserAsync(string userId, RoleMappingDTO role)
{
var tokenResponse = await GetAdminTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var addRoleUrl = $"http://localhost:8080/admin/realms/keycloacktest/users/{userId}/role-mappings/clients/{role.ContainerId}";
var content = new StringContent(JsonConvert.SerializeObject(new[] { new { id = role.Id, name = role.Name } }), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(addRoleUrl, content);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new HttpRequestException($"Failed to add role to user: {response.StatusCode}, {error}");
}
}
public async Task AddGroupRoleToUserAsync(string userId, string groupId, string roleName)
{
var roles = await GetRolesByGroupIdAsync(groupId);
var role = roles.FirstOrDefault(r => r.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase));
if (role == null)
{
throw new Exception($"Role '{roleName}' not found in group '{groupId}'");
}
await AddRoleToUserAsync(userId, role);
}
Note that in many of these requests, I'm obtaining the token from a user with the 'manage-users' role mapped and using it in the requests. If you want to take a closer look at this code and see the DTOs, check out this example implementation I put together on GitHub: https://github.com/Andrei-hub11/ASPNetCoreKeycloakExample/blob/main/Services/AccountService.cs
Upvotes: 0