11#! /usr/bin/env bash
2- # test-lab-02-04.sh — Lab 02- 04: SSO Integration
3- # Module 02: Keycloak OAuth2/ OIDC/ SAML SSO provider
4- # keycloak with Keycloak OIDC/SAML authentication
2+ # test-lab-02-04.sh — Keycloak Lab 04: Full OIDC/SAML SSO Hub
3+ # Tests: realm config, OIDC client flows, SAML metadata, ROPC grant,
4+ # refresh tokens, JWT decode, token introspection, MailHog email
55set -euo pipefail
66
7- LAB_ID=" 02-04"
8- LAB_NAME=" SSO Integration"
9- MODULE=" keycloak"
10- COMPOSE_FILE=" docker/docker-compose.sso.yml"
11- PASS=0
12- FAIL=0
13-
14- # ── Colors ────────────────────────────────────────────────────────────────────
15- RED=' \033[0;31m' ; GREEN=' \033[0;32m' ; YELLOW=' \033[1;33m'
16- CYAN=' \033[0;36m' ; NC=' \033[0m'
17-
18- pass () { echo -e " ${GREEN} [PASS]${NC} $1 " ; (( PASS++ )) ; }
19- fail () { echo -e " ${RED} [FAIL]${NC} $1 " ; (( FAIL++ )) ; }
20- info () { echo -e " ${CYAN} [INFO]${NC} $1 " ; }
21- warn () { echo -e " ${YELLOW} [WARN]${NC} $1 " ; }
22-
23- echo -e " ${CYAN} ======================================${NC} "
24- echo -e " ${CYAN} Lab ${LAB_ID} : ${LAB_NAME}${NC} "
25- echo -e " ${CYAN} Module: ${MODULE}${NC} "
26- echo -e " ${CYAN} ======================================${NC} "
27- echo " "
28-
29- # ── PHASE 1: Setup ────────────────────────────────────────────────────────────
30- info " Phase 1: Setup"
31- docker compose -f " ${COMPOSE_FILE} " up -d
32- info " Waiting 30s for ${MODULE} to initialize..."
33- sleep 30
34-
35- # ── PHASE 2: Health Checks ────────────────────────────────────────────────────
36- info " Phase 2: Health Checks"
37-
38- if docker compose -f " ${COMPOSE_FILE} " ps | grep -q " running\|Up" ; then
39- pass " Container is running"
7+ PASS=0; FAIL=0
8+ KC_PASS=" ${KC_PASS:- Lab04Password! } "
9+ KC_URL=" http://localhost:8080"
10+ REALM=" it-stack"
11+
12+ pass () { (( ++ PASS)) ; echo " [PASS] $1 " ; }
13+ fail () { (( ++ FAIL)) ; echo " [FAIL] $1 " ; }
14+ warn () { echo " [WARN] $1 " ; }
15+ header (){ echo ; echo " === $1 ===" ; }
16+
17+ kc_token () {
18+ curl -sf -X POST " $KC_URL /realms/master/protocol/openid-connect/token" \
19+ -d " client_id=admin-cli&grant_type=password&username=admin&password=${KC_PASS} " \
20+ | grep -o ' "access_token":"[^"]*"' | cut -d' "' -f4
21+ }
22+
23+ header " 1. Keycloak Health"
24+ if curl -sf " $KC_URL /health/ready" | grep -q ' "status":"UP"' ; then
25+ pass " Keycloak /health/ready UP"
26+ else
27+ fail " Keycloak not ready" ; exit 1
28+ fi
29+
30+ header " 2. Admin Authentication"
31+ TOKEN=$( kc_token)
32+ [[ -n " $TOKEN " ]] && pass " Admin token from master realm" || { fail " Admin auth failed" ; exit 1; }
33+
34+ header " 3. Realm Creation + Config"
35+ curl -sf -X POST " $KC_URL /admin/realms" \
36+ -H " Authorization: Bearer $TOKEN " -H " Content-Type: application/json" \
37+ -d " {\" realm\" :\" $REALM \" ,\" enabled\" :true,\" displayName\" :\" IT-Stack\" ,
38+ \" bruteForceProtected\" :true,\" ssoSessionMaxLifespan\" :86400}" \
39+ -o /dev/null && pass " Realm '$REALM ' created" || warn " Realm may exist"
40+
41+ TOKEN=$( kc_token)
42+ REALM_INFO=$( curl -sf " $KC_URL /admin/realms/$REALM " -H " Authorization: Bearer $TOKEN " )
43+ echo " $REALM_INFO " | grep -q ' "enabled":true' && pass " Realm is enabled" || fail " Realm not enabled"
44+ echo " $REALM_INFO " | grep -q ' "bruteForceProtected":true' && pass " Brute force protection enabled" || fail " Brute force not enabled"
45+
46+ header " 4. OIDC Client (confidential + service account + ROPC)"
47+ TOKEN=$( kc_token)
48+ STATUS=$( curl -s -o /dev/null -w " %{http_code}" -X POST " $KC_URL /admin/realms/$REALM /clients" \
49+ -H " Authorization: Bearer $TOKEN " -H " Content-Type: application/json" \
50+ -d " {\" clientId\" :\" oidc-client\" ,\" secret\" :\" $KC_PASS \" ,\" publicClient\" :false,
51+ \" serviceAccountsEnabled\" :true,\" directAccessGrantsEnabled\" :true,
52+ \" redirectUris\" :[\" http://localhost:9000/*\" ],\" enabled\" :true}" )
53+ [[ " $STATUS " =~ ^(201| 409)$ ]] && pass " OIDC client 'oidc-client' ready (HTTP $STATUS )" || fail " OIDC client failed (HTTP $STATUS )"
54+
55+ header " 5. SAML Client Registration"
56+ TOKEN=$( kc_token)
57+ STATUS=$( curl -s -o /dev/null -w " %{http_code}" -X POST " $KC_URL /admin/realms/$REALM /clients" \
58+ -H " Authorization: Bearer $TOKEN " -H " Content-Type: application/json" \
59+ -d " {\" clientId\" :\" saml-client\" ,\" protocol\" :\" saml\" ,
60+ \" redirectUris\" :[\" http://localhost:9001/*\" ],\" enabled\" :true}" )
61+ [[ " $STATUS " =~ ^(201| 409)$ ]] && pass " SAML client 'saml-client' ready (HTTP $STATUS )" || fail " SAML client failed (HTTP $STATUS )"
62+
63+ header " 6. Test User Creation"
64+ TOKEN=$( kc_token)
65+ STATUS=$( curl -s -o /dev/null -w " %{http_code}" -X POST " $KC_URL /admin/realms/$REALM /users" \
66+ -H " Authorization: Bearer $TOKEN " -H " Content-Type: application/json" \
67+ -d " {\" username\" :\" labuser\" ,\" enabled\" :true,\" email\" :\" labuser@lab.local\" ,
68+ \" emailVerified\" :true,\" firstName\" :\" Lab\" ,\" lastName\" :\" User\" ,
69+ \" credentials\" :[{\" type\" :\" password\" ,\" value\" :\" $KC_PASS \" ,\" temporary\" :false}]}" )
70+ [[ " $STATUS " =~ ^(201| 409)$ ]] && pass " User 'labuser' ready (HTTP $STATUS )" || fail " User creation failed (HTTP $STATUS )"
71+
72+ header " 7. Client Credentials Grant (OAuth2 M2M)"
73+ SA_TOKEN=$( curl -sf -X POST " $KC_URL /realms/$REALM /protocol/openid-connect/token" \
74+ -d " client_id=oidc-client&client_secret=${KC_PASS} &grant_type=client_credentials" \
75+ | grep -o ' "access_token":"[^"]*"' | cut -d' "' -f4)
76+ [[ -n " $SA_TOKEN " ]] && pass " Client credentials grant: access token obtained" || fail " Client credentials grant failed"
77+
78+ header " 8. JWT Structure + Claims"
79+ IFS=' .' read -ra P <<< " $SA_TOKEN"
80+ [[ " ${# P[@]} " -eq 3 ]] && pass " JWT has 3 parts (header.payload.sig)" || fail " Invalid JWT structure"
81+ if [[ " ${# P[@]} " -eq 3 ]]; then
82+ PAD=$(( 4 - ${# P[1]} % 4 )) ; [[ " $PAD " -lt 4 ]] && P[1]+=$( printf ' %0.s=' $( seq 1 $PAD ) )
83+ PAYLOAD=$( echo " ${P[1]} " | base64 -d 2> /dev/null || true)
84+ for claim in iss exp iat; do
85+ echo " $PAYLOAD " | grep -q " \" $claim \" " && pass " JWT claim '$claim ' present" || fail " JWT missing '$claim '"
86+ done
87+ fi
88+
89+ header " 9. Resource Owner Password Credentials (user login)"
90+ ROPC=$( curl -sf -X POST " $KC_URL /realms/$REALM /protocol/openid-connect/token" \
91+ -d " client_id=oidc-client&client_secret=${KC_PASS} &grant_type=password&username=labuser&password=${KC_PASS} " \
92+ 2> /dev/null || echo " {}" )
93+ echo " $ROPC " | grep -q ' "access_token"' && pass " ROPC grant: user token obtained" || fail " ROPC grant failed"
94+ REFRESH=$( echo " $ROPC " | grep -o ' "refresh_token":"[^"]*"' | cut -d' "' -f4 || true)
95+
96+ header " 10. Token Refresh"
97+ if [[ -n " $REFRESH " ]]; then
98+ REFRESHED=$( curl -sf -X POST " $KC_URL /realms/$REALM /protocol/openid-connect/token" \
99+ -d " client_id=oidc-client&client_secret=${KC_PASS} &grant_type=refresh_token&refresh_token=${REFRESH} " \
100+ | grep -o ' "access_token":"[^"]*"' | cut -d' "' -f4 || true)
101+ [[ -n " $REFRESHED " ]] && pass " Token refresh succeeded" || fail " Token refresh failed"
40102else
41- fail " Container is not running "
103+ fail " No refresh token to test "
42104fi
43105
44- # ── PHASE 3: Functional Tests ─────────────────────────────────────────────────
45- info " Phase 3: Functional Tests (Lab 04 — SSO Integration)"
46-
47- # TODO: Add module-specific functional tests here
48- # Example:
49- # if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
50- # pass "Health endpoint responds"
51- # else
52- # fail "Health endpoint not reachable"
53- # fi
54-
55- warn " Functional tests for Lab 02-04 pending implementation"
56-
57- # ── PHASE 4: Cleanup ──────────────────────────────────────────────────────────
58- info " Phase 4: Cleanup"
59- docker compose -f " ${COMPOSE_FILE} " down -v --remove-orphans
60- info " Cleanup complete"
61-
62- # ── Results ───────────────────────────────────────────────────────────────────
63- echo " "
64- echo -e " ${CYAN} ======================================${NC} "
65- echo -e " Lab ${LAB_ID} Complete"
66- echo -e " ${GREEN} PASS: ${PASS}${NC} | ${RED} FAIL: ${FAIL}${NC} "
67- echo -e " ${CYAN} ======================================${NC} "
68-
69- if [ " ${FAIL} " -gt 0 ]; then
70- exit 1
106+ header " 11. Token Introspection"
107+ TOKEN=$( kc_token)
108+ INTRO=$( curl -sf -X POST " $KC_URL /realms/$REALM /protocol/openid-connect/token/introspect" \
109+ -u " oidc-client:${KC_PASS} " -d " token=${SA_TOKEN} " | grep -o ' "active":[a-z]*' )
110+ echo " $INTRO " | grep -q ' "active":true' && pass " Token introspection: active=true" || fail " Token not active"
111+
112+ header " 12. OIDC Discovery"
113+ DISC=$( curl -sf " $KC_URL /realms/$REALM /.well-known/openid-configuration" )
114+ for f in token_endpoint authorization_endpoint jwks_uri userinfo_endpoint introspection_endpoint; do
115+ echo " $DISC " | grep -q " \" $f \" " && pass " Discovery: $f present" || fail " Discovery missing $f "
116+ done
117+
118+ header " 13. SAML Metadata Endpoint"
119+ SAML_META=$( curl -sf " $KC_URL /realms/$REALM /protocol/saml/descriptor" )
120+ echo " $SAML_META " | grep -q " EntityDescriptor\|IDPSSODescriptor" \
121+ && pass " SAML metadata XML returned" || fail " SAML metadata not available"
122+
123+ header " 14. Client List (verify both OIDC + SAML present)"
124+ TOKEN=$( kc_token)
125+ CLIENTS=$( curl -sf " $KC_URL /admin/realms/$REALM /clients" -H " Authorization: Bearer $TOKEN " )
126+ echo " $CLIENTS " | grep -q ' "oidc-client"' && pass " OIDC client visible in realm" || fail " OIDC client not found"
127+ echo " $CLIENTS " | grep -q ' "saml-client"' && pass " SAML client visible in realm" || fail " SAML client not found"
128+
129+ header " 15. MailHog Email Sink"
130+ if curl -sf http://localhost:8025/ -o /dev/null; then
131+ pass " MailHog UI accessible (:8025)"
132+ curl -sf http://localhost:8025/api/v2/messages | grep -q ' "total"' \
133+ && pass " MailHog API v2 messages endpoint works" || fail " MailHog API failed"
134+ else
135+ fail " MailHog not accessible"
71136fi
137+
138+ echo
139+ echo " ═══════════════════════════════════════"
140+ echo " Lab 02-04 Results: $PASS passed, $FAIL failed"
141+ echo " ═══════════════════════════════════════"
142+ [[ " $FAIL " -eq 0 ]]
0 commit comments