Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Keycloak.Net.Core.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{A56F65F6
ProjectSection(SolutionItems) = preProject
build\build.ps1 = build\build.ps1
build\test.ps1 = build\test.ps1
build\start-keycloak.sh = build\start-keycloak.sh
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{04ECC844-41D5-4A2B-85F1-DA1E3B3C58B3}"
ProjectSection(SolutionItems) = preProject
test\insurance-realm-export.json = test\insurance-realm-export.json
test\keycloak-net-fixture-realm-export.json = test\keycloak-net-fixture-realm-export.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{46B1B298-8884-4FE7-831D-FBCA62E10A72}"
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* changed ClientConfig to Dictionary<string, string>
* removed signing
* .NET 8 and .NET 10 support only
* updated for keycloak version 25+
* updated for keycloak version 26+
* added support for changing default `AdminClientId` which has default `admin-cli` value
* added support for System.Text.Json in replacement of NewtonsoftJson.

Expand All @@ -34,9 +34,9 @@
);
```

C# client for [Keycloak](https://www.keycloak.org/) 6.x
C# client for [Keycloak](https://www.keycloak.org/) 26.x

See documentation at [https://www.keycloak.org/docs-api/6.0/rest-api/](https://www.keycloak.org/docs-api/6.0/rest-api/)
See documentation at [https://www.keycloak.org/docs-api/latest/rest-api/](https://www.keycloak.org/docs-api/latest/rest-api/)

## Features
* [X] Attack Detection
Expand All @@ -63,8 +63,20 @@ See documentation at [https://www.keycloak.org/docs-api/6.0/rest-api/](https://w

## Testing

In order to run the tests, all it's needed is to have a running instance of Keycloak with (preferably) the `master` realm credentials admin/admin (as it's currently configured in the `/test/Keycloak.Net.Core.Tests/appsettings.json`) and create a new realm `Insurance` by importing the file in `/test/insurance-real-export.json`, which also has its admin user with the same credentials as mentioned before.
The easiest way to run the tests is to use the fixture launcher:

Then it's just as easy as running the tests.
```bash
./build/start-keycloak.sh --test --auto-cleanup
```

If for some reason you want to change the credentials, you need to make sure both realms have the same user and password as the tests use the same credentials for both `master` and `Insurance` realms.
This starts Keycloak 26.7.0 in Docker, imports `/test/keycloak-net-fixture-realm-export.json`, runs the tests, and removes the container when the run completes.

To start the fixture server without running tests:

```bash
./build/start-keycloak.sh
```

The fixture realm is `keycloak-net-fixture`. The tests use the credentials in `/test/Keycloak.Net.Core.Tests/appsettings.json`; the fixture export includes the matching realm admin user.

If you prefer to run Keycloak manually, import `/test/keycloak-net-fixture-realm-export.json` into a Keycloak 26.7.0 instance before running the tests. The imported realm must be available at `keycloak-net-fixture`, and the credentials in `/test/Keycloak.Net.Core.Tests/appsettings.json` must match the realm admin user included in the export.
172 changes: 172 additions & 0 deletions build/start-keycloak.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

CONTAINER_NAME="${KEYCLOAK_CONTAINER_NAME:-keycloak-net-tests}"
KEYCLOAK_VERSION="${KEYCLOAK_VERSION:-26.7.0}"
KEYCLOAK_IMAGE="${KEYCLOAK_IMAGE:-quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}}"
KEYCLOAK_PORT="${KEYCLOAK_PORT:-8080}"
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
REALM_EXPORT="${REALM_EXPORT:-${ROOT_DIR}/test/keycloak-net-fixture-realm-export.json}"
TEST_TARGET="${TEST_TARGET:-${ROOT_DIR}/Keycloak.Net.Core.sln}"
AUTO_CLEANUP=0
RUN_TESTS=0
DOTNET_TEST_ARGS=()

usage() {
cat <<USAGE
Usage: build/start-keycloak.sh [options] [-- dotnet-test-args]

Starts a Keycloak Docker container, imports the keycloak-net-fixture realm backup,
then waits until interrupted.

Options:
--test Run dotnet test after Keycloak is ready.
--auto-cleanup Remove the Keycloak container as soon as startup/tests complete.
-h, --help Show this help text.

Environment variables:
KEYCLOAK_CONTAINER_NAME Default: keycloak-net-tests
KEYCLOAK_VERSION Default: 26.7.0
KEYCLOAK_IMAGE Default: quay.io/keycloak/keycloak:\$KEYCLOAK_VERSION
KEYCLOAK_PORT Default: 8080
KEYCLOAK_ADMIN Default: admin
KEYCLOAK_ADMIN_PASSWORD Default: admin
REALM_EXPORT Default: test/keycloak-net-fixture-realm-export.json
TEST_TARGET Default: Keycloak.Net.Core.sln

Examples:
build/start-keycloak.sh
build/start-keycloak.sh --test
build/start-keycloak.sh --test --auto-cleanup
build/start-keycloak.sh --test -- --framework net8.0
USAGE
}

while (($#)); do
case "$1" in
--test)
RUN_TESTS=1
shift
;;
--auto-cleanup)
AUTO_CLEANUP=1
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
DOTNET_TEST_ARGS+=("$@")
break
;;
*)
DOTNET_TEST_ARGS+=("$1")
shift
;;
esac
done

if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required to run the Keycloak integration tests." >&2
exit 1
fi

if [[ "$RUN_TESTS" -eq 1 ]] && ! command -v dotnet >/dev/null 2>&1; then
echo ".NET SDK is required to run the test project." >&2
exit 1
fi

if [[ "$RUN_TESTS" -eq 0 && "${#DOTNET_TEST_ARGS[@]}" -gt 0 ]]; then
echo "dotnet test arguments were provided, but --test was not specified." >&2
echo "Use: build/start-keycloak.sh --test -- ${DOTNET_TEST_ARGS[*]}" >&2
exit 1
fi

if [[ ! -f "$REALM_EXPORT" ]]; then
echo "Realm export not found: $REALM_EXPORT" >&2
exit 1
fi

cleanup() {
local exit_code=$?

docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
exit "$exit_code"
}

wait_for_interrupt() {
echo "Press Ctrl-C to stop Keycloak and remove the container."

while true; do
sleep 1
done
}

wait_for_keycloak() {
local url="http://localhost:${KEYCLOAK_PORT}/realms/master"
local retries=120

echo "Waiting for Keycloak at $url"

for ((attempt = 1; attempt <= retries; attempt++)); do
if curl --fail --silent --output /dev/null "$url"; then
echo "Keycloak is ready."
return 0
fi

sleep 1
done

echo "Keycloak did not become ready within ${retries}s." >&2
docker logs "$CONTAINER_NAME" >&2 || true
return 1
}

trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true

echo "Starting Keycloak ${KEYCLOAK_IMAGE} on port ${KEYCLOAK_PORT}"

docker run \
--detach \
--name "$CONTAINER_NAME" \
--publish "${KEYCLOAK_PORT}:8080" \
--env KEYCLOAK_ADMIN="$KEYCLOAK_ADMIN" \
--env KEYCLOAK_ADMIN_PASSWORD="$KEYCLOAK_ADMIN_PASSWORD" \
--volume "${REALM_EXPORT}:/opt/keycloak/data/import/keycloak-net-fixture-realm-export.json:ro" \
"$KEYCLOAK_IMAGE" \
start-dev --features=admin-fine-grained-authz:v1 --import-realm >/dev/null

wait_for_keycloak

echo "Keycloak is running at http://localhost:${KEYCLOAK_PORT}"

if [[ "$RUN_TESTS" -eq 1 ]]; then
test_exit_code=0

if ((${#DOTNET_TEST_ARGS[@]})); then
echo "Running tests: dotnet test ${TEST_TARGET} ${DOTNET_TEST_ARGS[*]}"
dotnet test "$TEST_TARGET" "${DOTNET_TEST_ARGS[@]}" || test_exit_code=$?
else
echo "Running tests: dotnet test ${TEST_TARGET}"
dotnet test "$TEST_TARGET" || test_exit_code=$?
fi

if [[ "$AUTO_CLEANUP" -eq 0 ]]; then
wait_for_interrupt
fi

exit "$test_exit_code"
else
if [[ "$AUTO_CLEANUP" -eq 0 ]]; then
wait_for_interrupt
fi
fi
4 changes: 3 additions & 1 deletion src/Keycloak.Net.Core/Models/Root/Category.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public enum Category
NameIdMapper,
[EnumMember(Value = "Audience mapper")]
AudienceMapper,
}
[EnumMember(Value = "AuthnContextClassRef mapper")]
AuthnContextClassRefMapper,
}
3 changes: 3 additions & 0 deletions src/Keycloak.Net.Core/Models/Root/Locale.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public enum Locale
Fi,
Hr,
Hu,
Hy,
Id,
It,
Ja,
Kk,
Expand All @@ -42,6 +44,7 @@ public enum Locale
Th,
Tr,
Uk,
Vi,
[EnumMember(Value = "zh-CN")]
ZhCn,
[EnumMember(Value = "zh-TW")]
Expand Down
4 changes: 2 additions & 2 deletions src/Keycloak.Net.Core/RealmsAdmin/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public async Task<bool> UpdateRealmEventsProviderConfigurationAsync(string realm
public async Task<Group> GetRealmGroupByPathAsync(string realm,
string path,
CancellationToken cancellationToken = default) =>
await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/group-by-path/{path}")
await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/group-by-path/{path.TrimStart('/')}")
.GetJsonAsync<Group>(cancellationToken: cancellationToken)
.ConfigureAwait(false);

Expand Down Expand Up @@ -371,4 +371,4 @@ await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/users-manageme
.PutJsonAsync(managementPermission, cancellationToken: cancellationToken)
.ReceiveJson<ManagementPermission>()
.ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ namespace Keycloak.Net.Tests
{
public partial class KeycloakClientShould
{
[Theory]
[InlineData("Insurance", "vermeulen")]
public async Task GetUserNameStatusInBruteForceDetectionAsync(string realm, string search)
[Fact]
public async Task GetUserNameStatusInBruteForceDetectionAsync()
{
var users = await _client.GetUsersAsync(realm, search: search).ConfigureAwait(false);
string userId = users.FirstOrDefault()?.Id;
if (userId != null)
{
var result = await _client.GetUserNameStatusInBruteForceDetectionAsync(realm, userId).ConfigureAwait(false);
Assert.NotNull(result);
}
var realm = KeycloakTestFixture.Realm;
var userId = await _fixture.UserIdAsync();

var result = await _client.GetUserNameStatusInBruteForceDetectionAsync(realm, userId);

Assert.Equal(0, result.NumFailures);
Assert.False(result.Disabled);
Assert.Equal(0, result.LastFailure);
Assert.Equal("n/a", result.LastIpFailure);
}
}
}
Loading