2020-10-13 09:44:02 +00:00
|
|
|
# This tests Keycloak: it starts the service, creates a realm with an
|
|
|
|
# OIDC client and a user, and simulates the user logging in to the
|
|
|
|
# client using their Keycloak login.
|
|
|
|
|
2020-10-26 14:33:57 +00:00
|
|
|
let
|
2021-05-14 12:39:03 +00:00
|
|
|
certs = import ./common/acme/server/snakeoil-certs.nix;
|
2022-04-05 16:59:05 +00:00
|
|
|
frontendUrl = "https://${certs.domain}";
|
2020-10-26 14:33:57 +00:00
|
|
|
initialAdminPassword = "h4IhoJFnt2iQIR9";
|
|
|
|
|
|
|
|
keycloakTest = import ./make-test-python.nix (
|
|
|
|
{ pkgs, databaseType, ... }:
|
2020-10-13 09:44:02 +00:00
|
|
|
{
|
|
|
|
name = "keycloak";
|
2021-01-10 19:08:30 +00:00
|
|
|
meta = with pkgs.lib.maintainers; {
|
2020-10-13 09:44:02 +00:00
|
|
|
maintainers = [ talyz ];
|
|
|
|
};
|
|
|
|
|
|
|
|
nodes = {
|
2021-10-03 18:43:29 +00:00
|
|
|
keycloak = { config, ... }: {
|
2021-05-14 12:39:03 +00:00
|
|
|
security.pki.certificateFiles = [
|
|
|
|
certs.ca.cert
|
|
|
|
];
|
|
|
|
|
|
|
|
networking.extraHosts = ''
|
|
|
|
127.0.0.1 ${certs.domain}
|
|
|
|
'';
|
|
|
|
|
2020-10-13 09:44:02 +00:00
|
|
|
services.keycloak = {
|
|
|
|
enable = true;
|
2022-04-05 16:59:05 +00:00
|
|
|
settings = {
|
|
|
|
hostname = certs.domain;
|
|
|
|
};
|
|
|
|
inherit initialAdminPassword;
|
|
|
|
sslCertificate = "${certs.${certs.domain}.cert}";
|
|
|
|
sslCertificateKey = "${certs.${certs.domain}.key}";
|
2021-05-14 10:15:44 +00:00
|
|
|
database = {
|
|
|
|
type = databaseType;
|
|
|
|
username = "bogus";
|
2022-04-05 16:59:05 +00:00
|
|
|
name = "also bogus";
|
|
|
|
passwordFile = "${pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"}";
|
2021-05-14 10:15:44 +00:00
|
|
|
};
|
2021-10-03 18:43:29 +00:00
|
|
|
plugins = with config.services.keycloak.package.plugins; [
|
|
|
|
keycloak-discord
|
2022-03-22 23:57:11 +00:00
|
|
|
keycloak-metrics-spi
|
2021-10-03 18:43:29 +00:00
|
|
|
];
|
2020-10-13 09:44:02 +00:00
|
|
|
};
|
|
|
|
environment.systemPackages = with pkgs; [
|
|
|
|
xmlstarlet
|
2022-02-27 15:38:14 +00:00
|
|
|
html-tidy
|
2020-10-13 09:44:02 +00:00
|
|
|
jq
|
|
|
|
];
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
testScript =
|
|
|
|
let
|
|
|
|
client = {
|
|
|
|
clientId = "test-client";
|
|
|
|
name = "test-client";
|
|
|
|
redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ];
|
|
|
|
};
|
|
|
|
|
|
|
|
user = {
|
|
|
|
firstName = "Chuck";
|
|
|
|
lastName = "Testa";
|
|
|
|
username = "chuck.testa";
|
|
|
|
email = "chuck.testa@example.com";
|
|
|
|
};
|
|
|
|
|
|
|
|
password = "password1234";
|
|
|
|
|
|
|
|
realm = {
|
|
|
|
enabled = true;
|
|
|
|
realm = "test-realm";
|
|
|
|
clients = [ client ];
|
|
|
|
users = [(
|
|
|
|
user // {
|
|
|
|
enabled = true;
|
|
|
|
credentials = [{
|
|
|
|
type = "password";
|
|
|
|
temporary = false;
|
|
|
|
value = password;
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
)];
|
|
|
|
};
|
|
|
|
|
|
|
|
realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm);
|
|
|
|
|
|
|
|
jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" ''
|
|
|
|
if {
|
|
|
|
"firstName": .given_name,
|
|
|
|
"lastName": .family_name,
|
|
|
|
"username": .preferred_username,
|
|
|
|
"email": .email
|
|
|
|
} != ${builtins.toJSON user} then
|
|
|
|
error("Wrong user info!")
|
|
|
|
else
|
|
|
|
empty
|
|
|
|
end
|
|
|
|
'';
|
|
|
|
in ''
|
|
|
|
keycloak.start()
|
|
|
|
keycloak.wait_for_unit("keycloak.service")
|
2022-04-05 16:59:05 +00:00
|
|
|
keycloak.wait_for_open_port(443)
|
2020-10-13 09:44:02 +00:00
|
|
|
keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
|
|
|
|
|
|
|
|
### Realm Setup ###
|
|
|
|
|
|
|
|
# Get an admin interface access token
|
2022-03-22 23:57:11 +00:00
|
|
|
keycloak.succeed("""
|
|
|
|
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
|
|
|
|
""")
|
|
|
|
|
|
|
|
# Register the metrics SPI
|
2020-10-13 09:44:02 +00:00
|
|
|
keycloak.succeed(
|
2022-03-22 23:57:11 +00:00
|
|
|
"${pkgs.jre}/bin/keytool -import -alias snakeoil -file ${certs.ca.cert} -storepass aaaaaa -keystore cacert.jks -noprompt",
|
2022-04-05 16:59:05 +00:00
|
|
|
"KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh config credentials --server '${frontendUrl}' --realm master --user admin --password '${initialAdminPassword}'",
|
|
|
|
"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'",
|
2022-03-22 23:57:11 +00:00
|
|
|
"curl -sSf '${frontendUrl}/realms/master/metrics' | grep '^keycloak_admin_event_UPDATE'"
|
2020-10-13 09:44:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# Publish the realm, including a test OIDC client and user
|
|
|
|
keycloak.succeed(
|
|
|
|
"curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Generate and save the client secret. To do this we need
|
|
|
|
# Keycloak's internal id for the client.
|
|
|
|
keycloak.succeed(
|
|
|
|
"curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id",
|
|
|
|
"curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
### Authentication Testing ###
|
|
|
|
|
|
|
|
# Start the login process by sending an initial request to the
|
|
|
|
# OIDC authentication endpoint, saving the returned page. Tidy
|
|
|
|
# up the HTML (XmlStarlet is picky) and extract the login form
|
|
|
|
# post url.
|
|
|
|
keycloak.succeed(
|
|
|
|
"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",
|
2022-03-31 17:35:57 +00:00
|
|
|
"tidy -asxml -q -m login_form || true",
|
2020-10-13 09:44:02 +00:00
|
|
|
"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",
|
|
|
|
)
|
|
|
|
|
|
|
|
# Post the login form and save the response. Once again tidy up
|
|
|
|
# the HTML, then extract the authorization code.
|
|
|
|
keycloak.succeed(
|
|
|
|
"curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' \"$(<form_post_url)\" >auth_code_html",
|
2022-03-31 17:35:57 +00:00
|
|
|
"tidy -asxml -q -m auth_code_html || true",
|
2020-10-13 09:44:02 +00:00
|
|
|
"xml sel -T -t -m \"_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']\" -v @value auth_code_html >auth_code",
|
|
|
|
)
|
|
|
|
|
|
|
|
# Exchange the authorization code for an access token.
|
|
|
|
keycloak.succeed(
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Use the access token on the OIDC userinfo endpoint and check
|
|
|
|
# that the returned user info matches what we initialized the
|
|
|
|
# realm with.
|
|
|
|
keycloak.succeed(
|
|
|
|
"curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}"
|
|
|
|
)
|
|
|
|
'';
|
|
|
|
}
|
2020-10-26 14:33:57 +00:00
|
|
|
);
|
|
|
|
in
|
|
|
|
{
|
|
|
|
postgres = keycloakTest { databaseType = "postgresql"; };
|
2022-04-05 16:59:05 +00:00
|
|
|
mariadb = keycloakTest { databaseType = "mariadb"; };
|
2020-10-26 14:33:57 +00:00
|
|
|
mysql = keycloakTest { databaseType = "mysql"; };
|
|
|
|
}
|