Skip to content

Commit 4d99834

Browse files
committed
test(services/keycloak): OIDC password-grant end-to-end check
Boots a declared realm + user + public client (direct access grants only) and verifies the typed runtime options reach a working OIDC token endpoint: - password grant on the declared client returns access + id tokens - id_token + userinfo carry the declared claims (preferred_username, email, given/family/name, email_verified) - login_with_email_allowed lets the email substitute for the username - a wrong password is rejected (must fail) Container test (~45s), no new fixture dependencies.
1 parent 90b0509 commit 4d99834

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

services/keycloak/checks.nix

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,4 +664,124 @@ in
664664
assert flow.get("description") == "Passkey login flow"
665665
'';
666666
};
667+
668+
# OIDC password-grant end-to-end: declared user authenticates against a
669+
# declared client, and the returned id_token + userinfo carry the
670+
# standard claims the runtime config promised. Validates the typed
671+
# options -> .tf.json -> Keycloak API -> OIDC token pipeline end-to-end,
672+
# not just the admin-API surface the per-family tests cover.
673+
keycloak-e2e = pkgs.testers.runNixOSTest {
674+
name = "declarative-keycloak-e2e";
675+
676+
containers.keycloak = mkHost {
677+
runtime = {
678+
realms.acme = {
679+
display_name = "ACME";
680+
login_with_email_allowed = true;
681+
};
682+
683+
users.alice = {
684+
realm = "acme";
685+
username = "alice";
686+
email = "alice@acme.test";
687+
first_name = "Alice";
688+
last_name = "Tester";
689+
enabled = true;
690+
email_verified = true;
691+
initial_password = {
692+
valueFile = "/etc/secrets/alice-pw";
693+
temporary = false;
694+
};
695+
};
696+
697+
# PUBLIC client, only direct access grants enabled (the password
698+
# grant doesn't use redirects, so no valid_redirect_uris and
699+
# standard/implicit flow off -- the provider rejects redirect
700+
# URIs without a flow that uses them).
701+
openid_clients.test_app = {
702+
realm = "acme";
703+
client_id = "test-app";
704+
name = "Test App";
705+
access_type = "PUBLIC";
706+
standard_flow_enabled = false;
707+
direct_access_grants_enabled = true;
708+
};
709+
};
710+
extraEtc = {
711+
"secrets/alice-pw".text = "hackme";
712+
};
713+
};
714+
715+
testScript = ''
716+
${pyHelpers}
717+
import base64
718+
719+
def jwt_claims(tok):
720+
# JWT = header.payload.signature; payload is urlsafe-base64 JSON
721+
# (no padding). pad to a multiple of 4 before decoding.
722+
payload = tok.split(".")[1]
723+
payload += "=" * (-len(payload) % 4)
724+
return json.loads(base64.urlsafe_b64decode(payload))
725+
726+
start_all()
727+
keycloak.wait_for_unit("declarative-keycloak.service")
728+
729+
with subtest("password grant returns access + id token"):
730+
resp = json.loads(keycloak.succeed(
731+
"curl --fail -s -X POST "
732+
"http://localhost:8080/realms/acme/protocol/openid-connect/token "
733+
"-d grant_type=password "
734+
"-d client_id=test-app "
735+
"-d username=alice "
736+
"-d password=hackme "
737+
"--data-urlencode 'scope=openid email profile'"
738+
))
739+
assert "access_token" in resp, f"no access_token in response: {resp}"
740+
assert "id_token" in resp, f"no id_token in response: {resp}"
741+
access_token = resp["access_token"]
742+
id_token = resp["id_token"]
743+
744+
with subtest("id_token claims match declared user attributes"):
745+
c = jwt_claims(id_token)
746+
assert c.get("preferred_username") == "alice", c
747+
assert c.get("email") == "alice@acme.test", c
748+
assert c.get("email_verified") is True, c
749+
assert c.get("given_name") == "Alice", c
750+
assert c.get("family_name") == "Tester", c
751+
assert c.get("name") == "Alice Tester", c
752+
753+
with subtest("userinfo endpoint matches the id_token claims"):
754+
ui = json.loads(keycloak.succeed(
755+
f"curl --fail -s -H 'Authorization: Bearer {access_token}' "
756+
"http://localhost:8080/realms/acme/protocol/openid-connect/userinfo"
757+
))
758+
assert ui.get("preferred_username") == "alice", ui
759+
assert ui.get("email") == "alice@acme.test", ui
760+
assert ui.get("given_name") == "Alice", ui
761+
assert ui.get("family_name") == "Tester", ui
762+
763+
with subtest("login-with-email accepts the email as the username field"):
764+
resp2 = json.loads(keycloak.succeed(
765+
"curl --fail -s -X POST "
766+
"http://localhost:8080/realms/acme/protocol/openid-connect/token "
767+
"-d grant_type=password "
768+
"-d client_id=test-app "
769+
"-d username=alice@acme.test "
770+
"-d password=hackme "
771+
"--data-urlencode 'scope=openid'"
772+
))
773+
assert "access_token" in resp2, f"email login rejected: {resp2}"
774+
775+
with subtest("wrong password is rejected with 401"):
776+
# `curl --fail` exits non-zero on >= 400, so use machine.fail.
777+
keycloak.fail(
778+
"curl --fail -s -X POST "
779+
"http://localhost:8080/realms/acme/protocol/openid-connect/token "
780+
"-d grant_type=password "
781+
"-d client_id=test-app "
782+
"-d username=alice "
783+
"-d password=wrong"
784+
)
785+
'';
786+
};
667787
}

0 commit comments

Comments
 (0)