diff --git a/.devcontainer/post-create-script.sh b/.devcontainer/post-create-script.sh index 2b87675..c8d1686 100644 --- a/.devcontainer/post-create-script.sh +++ b/.devcontainer/post-create-script.sh @@ -9,6 +9,6 @@ wget -q -O - https://github.com/mike-engel/jwt-cli/releases/download/5.0.3/jwt-l # Install bats helpers [ -d tests/bats-helpers ] && rm -rf tests/bats-helpers && mkdir -p tests/bats-helpers -git clone --depth 1 https://github.com/bats-core/bats-support.git tests/bats-helpers/bats-support || true -git clone --depth 1 https://github.com/bats-core/bats-assert.git tests/bats-helpers/bats-assert || true -git clone --depth 1 https://github.com/bats-core/bats-file.git tests/bats-helpers/bats-file || true \ No newline at end of file +git clone --depth 1 https://github.com/bats-core/bats-support.git tests/bats-helpers/bats-support +git clone --depth 1 https://github.com/bats-core/bats-assert.git tests/bats-helpers/bats-assert +git clone --depth 1 https://github.com/bats-core/bats-file.git tests/bats-helpers/bats-file \ No newline at end of file diff --git a/auth/Dockerfile b/auth/Dockerfile index 5fca617..1335cd1 100644 --- a/auth/Dockerfile +++ b/auth/Dockerfile @@ -5,6 +5,8 @@ EXPOSE 8080 ENV JWT_ISSUER= ENV JWT_EXPIRY= ENV JWT_SECRET= +ENV JWT_ALLOWED_CLAIMS= +ENV JWT_REQUIRED_CLAIMS= # Install neccessary tools RUN apt-get update && apt-get install -y \ @@ -19,4 +21,4 @@ RUN wget -q -O - https://github.com/msoap/shell2http/releases/download/v1.16.0/s RUN wget -q -O - https://github.com/mike-engel/jwt-cli/releases/download/5.0.3/jwt-linux.tar.gz | tar xvz -C /usr/local/bin jwt ADD --chmod=a+x issue_jwt.sh issue_jwt.sh -CMD ["shell2http", "--500", "--form", "--export-all-vars", "POST:/", "./issue_jwt.sh"] \ No newline at end of file +CMD ["shell2http", "--cgi", "--show-errors", "--form", "--export-all-vars", "POST:/", "./issue_jwt.sh"] \ No newline at end of file diff --git a/auth/issue_jwt.sh b/auth/issue_jwt.sh index b0c032a..ce3b642 100755 --- a/auth/issue_jwt.sh +++ b/auth/issue_jwt.sh @@ -25,9 +25,63 @@ jsonify_prefixed_variables() { echo "$json" } +# Define a function the splits a comma separated string into the substrings +split_comma_separated_string() { + local input="$1" # Input comma-separated string + local IFS="," # Internal Field Separator set to comma + + # Loop through the input string and print elements + for item in $input; do + echo "$item" + done +} + +# Define a function that tests if array A is a subset of array B +# Returns 0 if A is a subset of B +# Returns 1 if A is NOT a subset of B +subset_of() { + local -n _array_A=$1 + local -n _array_B=$2 + _file_A=$(printf '%s\n' "${_array_A[@]}") + _file_B=$(printf '%s\n' "${_array_B[@]}") + + output=$(comm -23 <(sort <<< "$_file_A") <(sort <<< "$_file_B") | head -1) + if [[ -z $output ]]; then return 0; else return 1; fi +} + +## Start processing input + # Extract variables from shell2http input form_data_json=$(jsonify_prefixed_variables "v_") +# shellcheck disable=SC2034 +mapfile -t json_keys < <(echo "$form_data_json" | jq -r 'keys[]') + +# Read allowed claims from environment variable and +# verify form_data_json using allowed_claims. +# Return error if verification fails! +if [ -n "$JWT_ALLOWED_CLAIMS" ]; then + # shellcheck disable=SC2034 + mapfile -t allowed_claims < <(split_comma_separated_string "$JWT_ALLOWED_CLAIMS") + if ! subset_of json_keys allowed_claims; then + printf "%s\n\n%s\n" "Status: 400" "You provided: ${json_keys[*]} which is not a subset of the allowed ones: ${allowed_claims[*]}" + exit 1 + fi +fi + +# Read required claims from environment variable and +# verify form_data_json using required_claims. +# Return error if verification fails! +if [ -n "$JWT_REQUIRED_CLAIMS" ]; then + # shellcheck disable=SC2034 + mapfile -t required_claims < <(split_comma_separated_string "$JWT_REQUIRED_CLAIMS") + if ! subset_of required_claims json_keys; then + printf "%s\n\n%s\n" "Status: 400" "You provided: ${json_keys[*]} which is not a superset of the required ones: ${required_claims[*]}" + exit 1 + fi +fi + + # Extract variables from environment variables env_json=$(jsonify_prefixed_variables "JWT_CLAIM_") diff --git a/tests/20-test-issue-jwt.bats b/tests/20-test-issue-jwt.bats new file mode 100644 index 0000000..d87f3db --- /dev/null +++ b/tests/20-test-issue-jwt.bats @@ -0,0 +1,85 @@ +#!/usr/bin/env bats + +load "./bats-helpers/bats-support/load" +load "./bats-helpers/bats-assert/load" + +# The base setup is started before any test is executed and tore down after the +# last test finishes. In other words, all tests in this file run against the same +# base instance. + +setup_file() { + docker pull hivemq/mqtt-cli:4.15.0 + docker compose -f docker-compose.base.yml -f docker-compose.auth.yml -f tests/docker-compose.auth-test.yml up -d --build + sleep 10 +} + +teardown_file() { + docker compose -f docker-compose.base.yml -f docker-compose.auth.yml down --remove-orphans + docker container prune -f && docker volume prune -af +} + +@test "AUTH: token generation with ok params" { + + run curl -X POST --fail-with-body --location --silent localhost/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "param1=value1¶m2=value2&acl=dangerous" + + assert_equal "$status" 0 + + echo "$output" + + jwt_as_json=$(echo "$output" | jwt decode -j -) + + # For debug + echo "$jwt_as_json" + + field=$(echo "$jwt_as_json" | jq .header.typ) + assert_equal "$field" '"JWT"' + + field=$(echo "$jwt_as_json" | jq .header.alg) + assert_equal "$field" '"HS256"' + + field=$(echo "$jwt_as_json" | jq .payload.iss) + assert_equal "$field" '"pontos-hub"' + + field=$(echo "$jwt_as_json" | jq .payload.role) + assert_equal "$field" '"web_user"' + + field=$(echo "$jwt_as_json" | jq .payload.param1) + assert_equal "$field" '"value1"' + + field=$(echo "$jwt_as_json" | jq .payload.param2) + assert_equal "$field" '"value2"' + + # Check that field with the name acl gets overwritten appropriately + field=$(echo "$jwt_as_json" | jq .payload.sub) + assert_equal "$field" '"__token__"' + + # Check that field with the name acl gets overwritten appropriately + field=$(echo "$jwt_as_json" | jq .payload.acl) + assert_equal "$field" '""' +} + +@test "AUTH: token generation with non-allowed params" { + + run curl -X POST --fail-with-body --location localhost/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "param1=value1¬=allowed" + + assert_equal "$status" 22 + assert_line --partial 'The requested URL returned error: 400' + assert_line --partial 'You provided: not param1 which is not a subset of the allowed ones: param1 param2 param3 acl' + +} + +@test "AUTH: token generation without required params" { + + run curl -X POST --fail-with-body --location localhost/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "param2=value2" + + assert_equal "$status" 22 + assert_line --partial 'The requested URL returned error: 400' + assert_line --partial 'You provided: param2 which is not a superset of the required ones: param1' + +} \ No newline at end of file diff --git a/tests/20-test-auth-setup.bats b/tests/21-test-auth-setup.bats similarity index 84% rename from tests/20-test-auth-setup.bats rename to tests/21-test-auth-setup.bats index b4f2839..71de1a2 100644 --- a/tests/20-test-auth-setup.bats +++ b/tests/21-test-auth-setup.bats @@ -42,46 +42,6 @@ teardown_file() { assert_output --partial '200 OK' } -@test "AUTH: token generation" { - - run curl -X POST --location --silent localhost/token \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "param1=value1¶m2=value2&acl=dangerous" - - assert_equal "$status" 0 - - jwt_as_json=$(echo "$output" | jwt decode -j -) - - # For debug - echo "$jwt_as_json" - - field=$(echo "$jwt_as_json" | jq .header.typ) - assert_equal "$field" '"JWT"' - - field=$(echo "$jwt_as_json" | jq .header.alg) - assert_equal "$field" '"HS256"' - - field=$(echo "$jwt_as_json" | jq .payload.iss) - assert_equal "$field" '"pontos-hub"' - - field=$(echo "$jwt_as_json" | jq .payload.role) - assert_equal "$field" '"web_user"' - - field=$(echo "$jwt_as_json" | jq .payload.param1) - assert_equal "$field" '"value1"' - - field=$(echo "$jwt_as_json" | jq .payload.param2) - assert_equal "$field" '"value2"' - - # Check that field with the name acl gets overwritten appropriately - field=$(echo "$jwt_as_json" | jq .payload.sub) - assert_equal "$field" '"__token__"' - - # Check that field with the name acl gets overwritten appropriately - field=$(echo "$jwt_as_json" | jq .payload.acl) - assert_equal "$field" '""' -} - @test "AUTH: REST API access" { # Should not work as-is diff --git a/tests/21-test-setup-with-custom-acl-rules.bats b/tests/22-test-setup-with-custom-acl-rules.bats similarity index 100% rename from tests/21-test-setup-with-custom-acl-rules.bats rename to tests/22-test-setup-with-custom-acl-rules.bats diff --git a/tests/docker-compose.auth-test.yml b/tests/docker-compose.auth-test.yml new file mode 100644 index 0000000..236c214 --- /dev/null +++ b/tests/docker-compose.auth-test.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + + # JWT issuer + jwt: + build: + context: ./auth + restart: unless-stopped + environment: + - JWT_REQUIRED_CLAIMS=param1 + - JWT_ALLOWED_CLAIMS=param1,param2,param3,acl