From 31fe90d6effeb480057b400a9d7bf976021626a0 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 13 Oct 2020 11:44:02 +0200 Subject: [PATCH] nixos/keycloak: Add test --- nixos/tests/all-tests.nix | 1 + nixos/tests/keycloak.nix | 139 ++++++++++++++++++++++++++++++ pkgs/servers/keycloak/default.nix | 4 +- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/keycloak.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9ffeba27a7fe..5a10f60fc9a3 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -175,6 +175,7 @@ in kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; kernel-testing = handleTest ./kernel-testing.nix {}; + keycloak = handleTest ./keycloak.nix {}; keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; krb5 = discoverTests (import ./krb5 {}); diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix new file mode 100644 index 000000000000..e5e31b038e9c --- /dev/null +++ b/nixos/tests/keycloak.nix @@ -0,0 +1,139 @@ +# This tests Keycloak: it starts the service, creates a realm with an +# OIDC client and a user, and simulates the user logging in to the +# client using their Keycloak login. + +import ./make-test-python.nix ( + { pkgs, ... }: + let + frontendUrl = "http://keycloak/auth"; + initialAdminPassword = "h4IhoJFnt2iQIR9"; + in + { + name = "keycloak"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ talyz ]; + }; + + nodes = { + keycloak = { ... }: { + virtualisation.memorySize = 1024; + services.keycloak = { + enable = true; + inherit frontendUrl initialAdminPassword; + databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; + }; + environment.systemPackages = with pkgs; [ + xmlstarlet + libtidy + jq + ]; + }; + }; + + testScript = + let + client = { + clientId = "test-client"; + name = "test-client"; + redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ]; + }; + + user = { + firstName = "Chuck"; + lastName = "Testa"; + username = "chuck.testa"; + email = "chuck.testa@example.com"; + }; + + password = "password1234"; + + realm = { + enabled = true; + realm = "test-realm"; + clients = [ client ]; + users = [( + user // { + enabled = true; + credentials = [{ + type = "password"; + temporary = false; + value = password; + }]; + } + )]; + }; + + realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm); + + jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" '' + if { + "firstName": .given_name, + "lastName": .family_name, + "username": .preferred_username, + "email": .email + } != ${builtins.toJSON user} then + error("Wrong user info!") + else + empty + end + ''; + in '' + keycloak.start() + keycloak.wait_for_unit("keycloak.service") + keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}") + + + ### Realm Setup ### + + # Get an admin interface access token + keycloak.succeed( + "curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >admin_auth_header" + ) + + # Publish the realm, including a test OIDC client and user + keycloak.succeed( + "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'" + ) + + # Generate and save the client secret. To do this we need + # Keycloak's internal id for the client. + keycloak.succeed( + "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id", + "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(client_secret", + ) + + + ### Authentication Testing ### + + # Start the login process by sending an initial request to the + # OIDC authentication endpoint, saving the returned page. Tidy + # up the HTML (XmlStarlet is picky) and extract the login form + # post url. + keycloak.succeed( + "curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form", + "tidy -q -m login_form || true", + "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']\" -v @action login_form >form_post_url", + ) + + # Post the login form and save the response. Once again tidy up + # the HTML, then extract the authorization code. + keycloak.succeed( + "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(auth_code_html", + "tidy -q -m auth_code_html || true", + "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code", + ) + + # Exchange the authorization code for an access token. + keycloak.succeed( + "curl -sSf -d grant_type=authorization_code -d code=$(auth_header" + ) + + # Use the access token on the OIDC userinfo endpoint and check + # that the returned user info matches what we initialized the + # realm with. + keycloak.succeed( + "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}" + ) + ''; + } +) diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index c694b6a419f5..95935ce8f8ae 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -1,4 +1,4 @@ -{ stdenv, fetchzip, makeWrapper, jre, writeText +{ stdenv, fetchzip, makeWrapper, jre, writeText, nixosTests , postgresql_jdbc ? null }: @@ -50,6 +50,8 @@ stdenv.mkDerivation rec { wrapProgram $out/bin/jboss-cli.sh --set JAVA_HOME ${jre} ''; + passthru.tests = nixosTests.keycloak; + meta = with stdenv.lib; { homepage = "https://www.keycloak.org/"; description = "Identity and access management for modern applications and services";