at 21.11-pre 6.2 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}/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}