at master 7.8 kB view raw
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}