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