Release NixOS 23.11
[NixPkgs.git] / nixos / tests / keycloak.nix
blob228e57d1cdd6f75e99975aca4f45fc0d1c0f74bc
1 # This tests Keycloak: it starts the service, creates a realm with an
2 # OIDC client and a user, and simulates the user logging in to the
3 # client using their Keycloak login.
5 let
6   certs = import ./common/acme/server/snakeoil-certs.nix;
7   frontendUrl = "https://${certs.domain}";
9   keycloakTest = import ./make-test-python.nix (
10     { pkgs, databaseType, ... }:
11     let
12       initialAdminPassword = "h4Iho\"JFn't2>iQIR9";
13       adminPasswordFile = pkgs.writeText "admin-password" "${initialAdminPassword}";
14     in
15     {
16       name = "keycloak";
17       meta = with pkgs.lib.maintainers; {
18         maintainers = [ talyz ];
19       };
21       nodes = {
22         keycloak = { config, ... }: {
23           security.pki.certificateFiles = [
24             certs.ca.cert
25           ];
27           networking.extraHosts = ''
28             127.0.0.1 ${certs.domain}
29           '';
31           services.keycloak = {
32             enable = true;
33             settings = {
34               hostname = certs.domain;
35             };
36             inherit initialAdminPassword;
37             sslCertificate = "${certs.${certs.domain}.cert}";
38             sslCertificateKey = "${certs.${certs.domain}.key}";
39             database = {
40               type = databaseType;
41               username = "bogus";
42               name = "also bogus";
43               passwordFile = "${pkgs.writeText "dbPassword" ''wzf6\"vO"Cb\nP>p#6;c&o?eu=q'THE'''H''''E''}";
44             };
45             plugins = with config.services.keycloak.package.plugins; [
46               keycloak-discord
47               keycloak-metrics-spi
48             ];
49           };
50           environment.systemPackages = with pkgs; [
51             xmlstarlet
52             html-tidy
53             jq
54           ];
55         };
56       };
58       testScript =
59         let
60           client = {
61             clientId = "test-client";
62             name = "test-client";
63             redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
64           };
66           user = {
67             firstName = "Chuck";
68             lastName = "Testa";
69             username = "chuck.testa";
70             email = "chuck.testa@example.com";
71           };
73           password = "password1234";
75           realm = {
76             enabled = true;
77             realm = "test-realm";
78             clients = [ client ];
79             users = [(
80               user // {
81                 enabled = true;
82                 credentials = [{
83                   type = "password";
84                   temporary = false;
85                   value = password;
86                 }];
87               }
88             )];
89           };
91           realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
93           jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
94             if {
95               "firstName": .given_name,
96               "lastName": .family_name,
97               "username": .preferred_username,
98               "email": .email
99             } != ${builtins.toJSON user} then
100               error("Wrong user info!")
101             else
102               empty
103             end
104           '';
105         in ''
106           keycloak.start()
107           keycloak.wait_for_unit("keycloak.service")
108           keycloak.wait_for_open_port(443)
109           keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
111           ### Realm Setup ###
113           # Get an admin interface access token
114           keycloak.succeed("""
115               curl -sSf -d 'client_id=admin-cli' \
116                    -d 'username=admin' \
117                    -d "password=$(<${adminPasswordFile})" \
118                    -d 'grant_type=password' \
119                    '${frontendUrl}/realms/master/protocol/openid-connect/token' \
120                    | jq -r '"Authorization: bearer " + .access_token' >admin_auth_header
121           """)
123           # Register the metrics SPI
124           keycloak.succeed(
125               """${pkgs.jre}/bin/keytool -import -alias snakeoil -file ${certs.ca.cert} -storepass aaaaaa -keystore cacert.jks -noprompt""",
126               """KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh config credentials --server '${frontendUrl}' --realm master --user admin --password "$(<${adminPasswordFile})" """,
127               """KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh update events/config -s 'eventsEnabled=true' -s 'adminEventsEnabled=true' -s 'eventsListeners+=metrics-listener'""",
128               """curl -sSf '${frontendUrl}/realms/master/metrics' | grep '^keycloak_admin_event_UPDATE'"""
129           )
131           # Publish the realm, including a test OIDC client and user
132           keycloak.succeed(
133               "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
134           )
136           # Generate and save the client secret. To do this we need
137           # Keycloak's internal id for the client.
138           keycloak.succeed(
139               "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
140               "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
141           )
144           ### Authentication Testing ###
146           # Start the login process by sending an initial request to the
147           # OIDC authentication endpoint, saving the returned page. Tidy
148           # up the HTML (XmlStarlet is picky) and extract the login form
149           # post url.
150           keycloak.succeed(
151               "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",
152               "tidy -asxml -q -m login_form || true",
153               "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",
154           )
156           # Post the login form and save the response. Once again tidy up
157           # the HTML, then extract the authorization code.
158           keycloak.succeed(
159               "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
160               "tidy -asxml -q -m auth_code_html || true",
161               "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
162           )
164           # Exchange the authorization code for an access token.
165           keycloak.succeed(
166               "curl -sSf -d grant_type=authorization_code -d code=$(<auth_code) -d client_id=${client.name} -d client_secret=$(<client_secret) -d redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/token' | jq -r '\"Authorization: bearer \" + .access_token' >auth_header"
167           )
169           # Use the access token on the OIDC userinfo endpoint and check
170           # that the returned user info matches what we initialized the
171           # realm with.
172           keycloak.succeed(
173               "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
174           )
175         '';
176     }
177   );
180   postgres = keycloakTest { databaseType = "postgresql"; };
181   mariadb = keycloakTest { databaseType = "mariadb"; };
182   mysql = keycloakTest { databaseType = "mysql"; };