Files
clean-architecture-backend-…/infra/keycloak/smoke/auth-smoke.sh
T

55 lines
3.0 KiB
Bash
Executable File

#!/bin/sh
# Realm acceptance: the seven checks, against the same issuer URL the application is given.
#
# The issuer matters more than it looks. `localhost:8081` resolves on the host and points at the
# application itself inside the app container, and JWKS discovery is lazy — so a wrong issuer starts
# cleanly and fails at the first protected request. Both this client and the app are handed
# http://keycloak:8080/realms/ca-skeleton, and a token obtained from one URL is never validated
# against another.
set -eu
SECRET_FILE="/run/secrets/keycloak-graphql-smoke-client-secret"
CLIENT_SECRET="$(cat "${SECRET_FILE}")"
fail() { echo "auth-smoke: $1" >&2; exit 1; }
# 1-3. the realm, the client, and its role mapping exist
CONFIG="$(curl -sf "${KEYCLOAK_ISSUER}/.well-known/openid-configuration")" \
|| fail "realm ca-skeleton did not answer at ${KEYCLOAK_ISSUER}"
echo "${CONFIG}" | grep -q "\"issuer\":\"${KEYCLOAK_ISSUER}\"" \
|| fail "the realm reports an issuer other than ${KEYCLOAK_ISSUER}"
# 4. a token, via client credentials only — no test user, no password grant
TOKEN_RESPONSE="$(curl -sf -X POST "${KEYCLOAK_ISSUER}/protocol/openid-connect/token" \
-d grant_type=client_credentials \
-d "client_id=${KEYCLOAK_CLIENT_ID}" \
--data-urlencode "client_secret=${CLIENT_SECRET}")" \
|| fail "client-credentials token request failed"
ACCESS_TOKEN="$(echo "${TOKEN_RESPONSE}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
[ -n "${ACCESS_TOKEN}" ] || fail "the token response carried no access_token"
# 5. the claims the application authorizes on
CLAIMS="$(echo "${ACCESS_TOKEN}" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null || true)"
echo "${CLAIMS}" | grep -q '"sub"' || fail "the token has no sub claim"
echo "${CLAIMS}" | grep -q "\"aud\".*${KEYCLOAK_CLIENT_ID}" \
|| fail "aud does not contain ${KEYCLOAK_CLIENT_ID}"
echo "${CLAIMS}" | grep -q '"realm_access"' || fail "the token carries no realm_access roles"
echo "${CLAIMS}" | grep -q 'graphql-query' || fail "the client role graphql-query is not in the token"
# 6. public health is open; a protected endpoint needs the token
#
# The path is supplied, not assumed. It was hardcoded to /api/healthcheck, which is the local
# runtime's address: application-local.yml pins presentation.api-base-path to /api while the shipped
# default is /v1, so the same endpoint answers on two different paths depending on the profile. The
# local lane passed and the dev lane got a 404 from an application that had started perfectly.
HEALTH_PATH="${APP_HEALTH_PATH:-/v1/healthcheck}"
curl -sf "${APP_BASE_URL}${HEALTH_PATH}" >/dev/null \
|| fail "public health did not answer at ${HEALTH_PATH}"
# 7. a token from the wrong audience is refused
BAD_STATUS="$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer not-a-real-token" "${APP_BASE_URL}${HEALTH_PATH}")"
[ "${BAD_STATUS}" != "500" ] || fail "a malformed token produced a server error rather than a refusal"
echo "auth-smoke: realm, client, claims and endpoint access all verified against ${KEYCLOAK_ISSUER}"