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}/auth";
8 initialAdminPassword = "h4IhoJFnt2iQIR9";
9
10 keycloakTest = import ./make-test-python.nix (
11 { pkgs, databaseType, ... }:
12 {
13 name = "keycloak";
14 meta = with pkgs.lib.maintainers; {
15 maintainers = [ talyz ];
16 };
17
18 nodes = {
19 keycloak = { ... }: {
20 virtualisation.memorySize = 1024;
21
22 security.pki.certificateFiles = [
23 certs.ca.cert
24 ];
25
26 networking.extraHosts = ''
27 127.0.0.1 ${certs.domain}
28 '';
29
30 services.keycloak = {
31 enable = true;
32 inherit frontendUrl initialAdminPassword;
33 sslCertificate = certs.${certs.domain}.cert;
34 sslCertificateKey = certs.${certs.domain}.key;
35 database = {
36 type = databaseType;
37 username = "bogus";
38 passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
39 };
40 };
41
42 environment.systemPackages = with pkgs; [
43 xmlstarlet
44 libtidy
45 jq
46 ];
47 };
48 };
49
50 testScript =
51 let
52 client = {
53 clientId = "test-client";
54 name = "test-client";
55 redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
56 };
57
58 user = {
59 firstName = "Chuck";
60 lastName = "Testa";
61 username = "chuck.testa";
62 email = "chuck.testa@example.com";
63 };
64
65 password = "password1234";
66
67 realm = {
68 enabled = true;
69 realm = "test-realm";
70 clients = [ client ];
71 users = [(
72 user // {
73 enabled = true;
74 credentials = [{
75 type = "password";
76 temporary = false;
77 value = password;
78 }];
79 }
80 )];
81 };
82
83 realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
84
85 jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
86 if {
87 "firstName": .given_name,
88 "lastName": .family_name,
89 "username": .preferred_username,
90 "email": .email
91 } != ${builtins.toJSON user} then
92 error("Wrong user info!")
93 else
94 empty
95 end
96 '';
97 in ''
98 keycloak.start()
99 keycloak.wait_for_unit("keycloak.service")
100 keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
101
102
103 ### Realm Setup ###
104
105 # Get an admin interface access token
106 keycloak.succeed(
107 "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"
108 )
109
110 # Publish the realm, including a test OIDC client and user
111 keycloak.succeed(
112 "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
113 )
114
115 # Generate and save the client secret. To do this we need
116 # Keycloak's internal id for the client.
117 keycloak.succeed(
118 "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
119 "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
120 )
121
122
123 ### Authentication Testing ###
124
125 # Start the login process by sending an initial request to the
126 # OIDC authentication endpoint, saving the returned page. Tidy
127 # up the HTML (XmlStarlet is picky) and extract the login form
128 # post url.
129 keycloak.succeed(
130 "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",
131 "tidy -q -m login_form || true",
132 "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",
133 )
134
135 # Post the login form and save the response. Once again tidy up
136 # the HTML, then extract the authorization code.
137 keycloak.succeed(
138 "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
139 "tidy -q -m auth_code_html || true",
140 "xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
141 )
142
143 # Exchange the authorization code for an access token.
144 keycloak.succeed(
145 "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"
146 )
147
148 # Use the access token on the OIDC userinfo endpoint and check
149 # that the returned user info matches what we initialized the
150 # realm with.
151 keycloak.succeed(
152 "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
153 )
154 '';
155 }
156 );
157in
158{
159 postgres = keycloakTest { databaseType = "postgresql"; };
160 mysql = keycloakTest { databaseType = "mysql"; };
161}