at 23.11-pre 7.6 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 = 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 }; 20 21 nodes = { 22 keycloak = { config, ... }: { 23 security.pki.certificateFiles = [ 24 certs.ca.cert 25 ]; 26 27 networking.extraHosts = '' 28 127.0.0.1 ${certs.domain} 29 ''; 30 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 }; 57 58 testScript = 59 let 60 client = { 61 clientId = "test-client"; 62 name = "test-client"; 63 redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ]; 64 }; 65 66 user = { 67 firstName = "Chuck"; 68 lastName = "Testa"; 69 username = "chuck.testa"; 70 email = "chuck.testa@example.com"; 71 }; 72 73 password = "password1234"; 74 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 }; 90 91 realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm); 92 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}") 110 111 ### Realm Setup ### 112 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 """) 122 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 ) 130 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 ) 135 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 ) 142 143 144 ### Authentication Testing ### 145 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 ) 155 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 ) 163 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 ) 168 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 ); 178in 179{ 180 postgres = keycloakTest { databaseType = "postgresql"; }; 181 mariadb = keycloakTest { databaseType = "mariadb"; }; 182 mysql = keycloakTest { databaseType = "mysql"; }; 183}