diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17ce3da..6304f8a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,37 +17,80 @@ jobs: - name: Start & configure Keycloak and debugger id: configure run: | + # Install testing dependencies + npm install --prefix tests + # Start Docker containers CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up -d --build sleep 30 - # Configure client credentials flow + # Configure Keycloak KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token') - curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}' - curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"name": "client-credentials-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}' - curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"clientId": "client-credentials", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}' - KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].id') - KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].clientId') - KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].secret') - KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .id') - KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .name') - curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID/optional-client-scopes/$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" - - # Share variables to next steps - echo "CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT=http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" >> $GITHUB_OUTPUT - echo "CLIENT_CREDENTIALS_CLIENT_ID=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID)" >> $GITHUB_OUTPUT - echo "CLIENT_CREDENTIALS_CLIENT_SECRET=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET)" >> $GITHUB_OUTPUT - echo "CLIENT_CREDENTIALS_SCOPE=$(echo $KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME)" >> $GITHUB_OUTPUT + curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}' + + for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC + do + FLOW_NAME=$(echo ${FLOW_VARIABLE} | tr '[:upper:]' '[:lower:]' | tr '_' '-') + + KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token') + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"name": "'${FLOW_NAME}'-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}' + case "${FLOW_VARIABLE}" in + CLIENT_CREDENTIALS) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}' + ;; + AUTHORIZATION_CODE_CONFIDENTIAL) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret", "frontchannelLogout": true, "redirectUris": ["http://localhost:3000/callback"], "webOrigins": ["/*", "http://localhost:3000/*"], "attributes": {"frontchannel.logout.url": "http://localhost:3000/logout"}}' + ;; + AUTHORIZATION_CODE_PUBLIC) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": true, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "clientAuthenticatorType": null, "frontchannelLogout": true, "redirectUris": ["http://localhost:3000/callback"], "webOrigins": ["/*", "http://localhost:3000/*"], "attributes": {"frontchannel.logout.url": "http://localhost:3000/logout"}}' + ;; + esac + + CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].id') + CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].clientId') + CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].secret') + SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id') + SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .name') + curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + USER_ID=$(curl -X POST "http://localhost:8080/admin/realms/debugger-testing/users" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"username": "'${FLOW_NAME}'", "firstName": "'${FLOW_NAME}'", "lastName": "'${FLOW_NAME}'", "email": "'${FLOW_NAME}'@iyasec.io", "enabled": true, "emailVerified": true}' -i | grep Location | rev | cut -d '/' -f 1 | rev | tr -d ' \n\r') + curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/users/${USER_ID}/reset-password" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"type": "password", "value": "'${FLOW_NAME}'", "temporary": false}' + + echo "${FLOW_VARIABLE}_DISCOVERY_ENDPOINT=http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" >> $GITHUB_OUTPUT + echo "${FLOW_VARIABLE}_CLIENT_ID=${CLIENT_CLIENTID}" >> $GITHUB_OUTPUT + echo "${FLOW_VARIABLE}_CLIENT_SECRET=${CLIENT_SECRET}" >> $GITHUB_OUTPUT + echo "${FLOW_VARIABLE}_SCOPE=${SCOPE_NAME}" >> $GITHUB_OUTPUT + echo "${FLOW_VARIABLE}_USER=${USER_ID}" >> $GITHUB_OUTPUT + done - name: Test client credentials flow id: test_client_credentials run: | - # Install dependencies - cd tests && npm install - - # Test client credentials flow DISCOVERY_ENDPOINT=${{ steps.configure.outputs.CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT }} \ CLIENT_ID=${{ steps.configure.outputs.CLIENT_CREDENTIALS_CLIENT_ID }} \ CLIENT_SECRET=${{ steps.configure.outputs.CLIENT_CREDENTIALS_CLIENT_SECRET }} \ SCOPE=${{ steps.configure.outputs.CLIENT_CREDENTIALS_SCOPE }} \ - node oauth2_client_credentials.js \ No newline at end of file + node tests/oauth2_client_credentials.js + + - name: Test authorization code flow + id: test_authorization_code + run: | + for PKCE_ENABLED in true false + do + # Confidential client + DISCOVERY_ENDPOINT=${{ steps.configure.outputs.AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT }} \ + CLIENT_ID=${{ steps.configure.outputs.AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID }} \ + CLIENT_SECRET=${{ steps.configure.outputs.AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET }} \ + SCOPE=${{ steps.configure.outputs.AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE }} \ + USER=${{ steps.configure.outputs.AUTHORIZATION_CODE_CONFIDENTIAL_USER }} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + node tests/oauth2_authorization_code.js + + # Public client + DISCOVERY_ENDPOINT=${{ steps.configure.outputs.AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT }} \ + CLIENT_ID=${{ steps.configure.outputs.AUTHORIZATION_CODE_PUBLIC_CLIENT_ID }} \ + CLIENT_SECRET=${{ steps.configure.outputs.AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET }} \ + SCOPE=${{ steps.configure.outputs.AUTHORIZATION_CODE_PUBLIC_SCOPE }} \ + USER=${{ steps.configure.outputs.AUTHORIZATION_CODE_PUBLIC_USER }} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + node tests/oauth2_authorization_code.js + done \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8ba5a5a..72ac23a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ api/node_modules client/node_modules -node_modules \ No newline at end of file +node_modules +.idea \ No newline at end of file diff --git a/api/server.js b/api/server.js index ec6e820..d13dd5e 100644 --- a/api/server.js +++ b/api/server.js @@ -197,6 +197,7 @@ app.post('/token', (req, res) => { parameterObject[key] + "&"; }); + var headers = { 'content-type' : 'application/x-www-form-urlencoded' }; @@ -277,5 +278,4 @@ let options = { expressSwagger(options) app.listen(PORT, HOST); -log.info(`Running on http://${HOST}:${PORT}`); - +log.info(`Running on http://${HOST}:${PORT}`); \ No newline at end of file diff --git a/run-tests.sh b/run-tests.sh index 09baa05..f021ccb 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,27 +1,75 @@ #!/bin/bash +# Install testing dependencies +npm install --prefix tests + # Start Docker containers CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up -d --build sleep 30 -# Configure client credentials flow +# Configure Keycloak KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token') -curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}' -curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"name": "client-credentials-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}' -curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"clientId": "client-credentials", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}' -KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].id') -KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].clientId') -KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=client-credentials" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[0].secret') -KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .id') -KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" | jq -r '.[] | select(.name=="client-credentials-scope") | .name') -curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_ID/optional-client-scopes/$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_ID" -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" - -# Install dependencies -cd tests && npm install +curl -X POST "http://localhost:8080/admin/realms" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"realm": "debugger-testing", "enabled": true}' + +for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC +do + FLOW_NAME=$(echo ${FLOW_VARIABLE} | tr '[:upper:]' '[:lower:]' | tr '_' '-') + + KEYCLOAK_ACCESS_TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "username=keycloak" -d "password=keycloak" -d "grant_type=password" | jq -r '.access_token') + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"name": "'${FLOW_NAME}'-scope", "protocol": "openid-connect", "attributes": {"display.on.consent.screen": "false", "include.in.token.scope": "true"}}' + case "${FLOW_VARIABLE}" in + CLIENT_CREDENTIALS) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": false, "standardFlowEnabled": false, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret"}' + ;; + AUTHORIZATION_CODE_CONFIDENTIAL) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": false, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "clientAuthenticatorType": "client-secret", "frontchannelLogout": true, "redirectUris": ["http://localhost:3000/callback"], "webOrigins": ["/*", "http://localhost:3000/*"], "attributes": {"frontchannel.logout.url": "http://localhost:3000/logout"}}' + ;; + AUTHORIZATION_CODE_PUBLIC) + curl -X POST "http://localhost:8080/admin/realms/debugger-testing/clients" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"clientId": "'${FLOW_NAME}'", "protocol": "openid-connect", "publicClient": true, "serviceAccountsEnabled": false, "authorizationServicesEnabled": false, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "clientAuthenticatorType": null, "frontchannelLogout": true, "redirectUris": ["http://localhost:3000/callback"], "webOrigins": ["/*", "http://localhost:3000/*"], "attributes": {"frontchannel.logout.url": "http://localhost:3000/logout"}}' + ;; + esac + + CLIENT_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].id') + CLIENT_CLIENTID=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].clientId') + CLIENT_SECRET=$(curl "http://localhost:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[0].secret') + SCOPE_ID=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id') + SCOPE_NAME=$(curl "http://localhost:8080/admin/realms/debugger-testing/client-scopes" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .name') + curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + USER_ID=$(curl -X POST "http://localhost:8080/admin/realms/debugger-testing/users" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"username": "'${FLOW_NAME}'", "firstName": "'${FLOW_NAME}'", "lastName": "'${FLOW_NAME}'", "email": "'${FLOW_NAME}'@iyasec.io", "enabled": true, "emailVerified": true}' -i | grep Location | rev | cut -d '/' -f 1 | rev | tr -d ' \n\r') + curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/users/${USER_ID}/reset-password" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{"type": "password", "value": "'${FLOW_NAME}'", "temporary": false}' + + declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" + declare ${FLOW_VARIABLE}_CLIENT_ID="${CLIENT_CLIENTID}" + declare ${FLOW_VARIABLE}_CLIENT_SECRET="${CLIENT_SECRET}" + declare ${FLOW_VARIABLE}_SCOPE="${SCOPE_NAME}" + declare ${FLOW_VARIABLE}_USER="${USER_ID}" +done # Test client credentials flow -DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" \ -CLIENT_ID=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_CLIENTID \ -CLIENT_SECRET=$KEYCLOAK_CLIENT_CREDENTIALS_CLIENT_SECRET \ -SCOPE=$KEYCLOAK_CLIENT_CREDENTIALS_SCOPE_NAME \ -node oauth2_client_credentials.js \ No newline at end of file +DISCOVERY_ENDPOINT=${CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT} \ +CLIENT_ID=${CLIENT_CREDENTIALS_CLIENT_ID} \ +CLIENT_SECRET=${CLIENT_CREDENTIALS_CLIENT_SECRET} \ +SCOPE=${CLIENT_CREDENTIALS_SCOPE} \ +node tests/oauth2_client_credentials.js + +# Test authorization code flow +for PKCE_ENABLED in true false +do + # Confidential client + DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT} \ + CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID} \ + CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET} \ + SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE} \ + USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + node tests/oauth2_authorization_code.js + + # Public client + DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT} \ + CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID} \ + CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET} \ + SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE} \ + USER=${AUTHORIZATION_CODE_PUBLIC_USER} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + node tests/oauth2_authorization_code.js +done \ No newline at end of file diff --git a/tests/oauth2_authorization_code.js b/tests/oauth2_authorization_code.js new file mode 100644 index 0000000..896625c --- /dev/null +++ b/tests/oauth2_authorization_code.js @@ -0,0 +1,178 @@ +const { Builder, By, until } = require("selenium-webdriver"); +const { Select } = require('selenium-webdriver/lib/select'); +const chrome = require("selenium-webdriver/chrome"); +const jwt = require("jsonwebtoken"); +const assert = require("assert"); + +async function populateMetadata(driver, discovery_endpoint) { + oidc_discovery_endpoint = By.id("oidc_discovery_endpoint"); + btn_oidc_discovery_endpoint = By.className("btn_oidc_discovery_endpoint"); + btn_oidc_populate_meta_data = By.className("btn_oidc_populate_meta_data"); + + // Wait until page is loaded + await driver.wait(until.elementLocated(oidc_discovery_endpoint), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(oidc_discovery_endpoint)), 10000); + + // Enter discovery endpoint + await driver.findElement(oidc_discovery_endpoint).clear(); + await driver.findElement(oidc_discovery_endpoint).sendKeys(discovery_endpoint); + await driver.findElement(btn_oidc_discovery_endpoint).click(); + + // Populate metadata + await driver.wait(until.elementLocated(btn_oidc_populate_meta_data), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(btn_oidc_populate_meta_data)), 10000); + await driver.executeScript("arguments[0].scrollIntoView({ behavior: 'smooth', block: 'center' });", await driver.findElement(btn_oidc_populate_meta_data)); + await driver.findElement(btn_oidc_populate_meta_data).click(); +} + +async function getAccessToken(driver, client_id, client_secret, scope, pkce_enabled) { + authorization_grant_type = By.id("authorization_grant_type"); + usePKCE_yes = By.id("usePKCE-yes"); + usePKCE_no = By.id("usePKCE-no"); + authz_expand_button = By.id("authz_expand_button"); + client_id_ = By.id("client_id"); + scope_ = By.id("scope"); + token_client_id = By.id("token_client_id"); + token_client_secret = By.id("token_client_secret"); + token_scope = By.id("token_scope"); + btn_authorize = By.css("input[type=\"submit\"][value=\"Authorize\"]"); + keycloak_username = By.id("username"); + keycloak_password = By.id("password"); + keycloak_kc_login = By.id("kc-login"); + btn1 = By.className("btn1"); + token_access_token = By.id("token_access_token"); + display_token_error_form_textarea1 = By.id("display_token_error_form_textarea1"); + + // Select client credential login type + await new Select(await driver.findElement(authorization_grant_type)).selectByVisibleText('OIDC Authorization Code Flow(code)'); + await driver.wait(until.elementLocated(usePKCE_yes), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(usePKCE_yes)), 10000); + await driver.wait(until.elementLocated(usePKCE_no), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(usePKCE_no)), 10000); + + if (pkce_enabled) { + await driver.findElement(usePKCE_yes).click(); + } else { + await driver.findElement(usePKCE_no).click(); + } + + await driver.wait(until.elementLocated(authz_expand_button), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000); + await driver.findElement(authz_expand_button).click(); + await driver.wait(until.elementLocated(client_id_), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(client_id_)), 10000); + + // Submit credentials + await driver.findElement(client_id_).clear(); + await driver.findElement(client_id_).sendKeys(client_id); + await driver.findElement(scope_).clear(); + await driver.findElement(scope_).sendKeys(scope); + await driver.findElement(btn_authorize).click(); + + // Login to Keycloak + try { + await driver.wait(until.elementLocated(keycloak_username), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(keycloak_username)), 10000); + } catch (error) { + authz_error_report = await driver.findElement(By.id("authz-error-report")); + authz_error_report_paragraphs = await authz_error_report.findElements(By.css("p")); + throw new Error(await authz_error_report_paragraphs[authz_error_report_paragraphs.length - 1].getText()); + } + + await driver.findElement(keycloak_username).clear(); + await driver.findElement(keycloak_username).sendKeys(client_id); + await driver.findElement(keycloak_password).clear(); + await driver.findElement(keycloak_password).sendKeys(client_id); + await driver.findElement(keycloak_kc_login).click(); + + // Submit credentials (again) + await driver.wait(until.elementLocated(token_client_id), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(token_client_id)), 10000); + + await driver.findElement(token_client_id).clear(); + await driver.findElement(token_client_id).sendKeys(client_id); + await driver.findElement(token_client_secret).clear(); + await driver.findElement(token_client_secret).sendKeys(client_secret); + await driver.findElement(token_scope).clear(); + await driver.findElement(token_scope).sendKeys(scope); + await driver.findElement(btn1).click(); + + // Get access token result + async function waitForVisibility(element) { + await driver.wait(until.elementLocated(element), 10000); + await driver.wait(until.elementIsVisible(driver.findElement(element)), 10000); + return element; + } + + let visibleAccessTokenElement = await Promise.any([ + waitForVisibility(token_access_token), + waitForVisibility(display_token_error_form_textarea1) + ]); + + return await driver.findElement(visibleAccessTokenElement).getAttribute("value"); +} + +async function verifyAccessToken(access_token, client_id, scope, user) { + async function compareScopes(scope1, scope2) { + scope1 = scope1.split(" "); + scope2 = scope2.split(" "); + + return scope2.every(element => scope1.includes(element)); + } + + let decoded_access_token = jwt.decode(access_token, { complete: true }); + let response_text = access_token.match(/responseText: (.*)/); + + assert.notStrictEqual(decoded_access_token, null, "Cannot decode access token. Request result: " + (response_text ? response_text[1] : "no response text")); + assert.strictEqual(decoded_access_token.payload.azp, client_id, "Access token AZP does not match client ID."); + assert.strictEqual(await compareScopes(decoded_access_token.payload.scope, scope), true, "Access token scope does not match scope."); + assert.strictEqual(decoded_access_token.payload.sub, user, "Access token SUB does not match user ID."); + assert.strictEqual(decoded_access_token.payload.given_name, client_id, "Access token given_name does not match."); + assert.strictEqual(decoded_access_token.payload.family_name, client_id, "Access token family_name does not match."); + assert.strictEqual(decoded_access_token.payload.email, `${client_id}@iyasec.io`, "Access token email does not match."); +} + +async function test() { + const options = new chrome.Options(); + options.addArguments("--headless"); + options.addArguments("--no-sandbox"); + const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build(); + + try { + const discovery_endpoint = process.env.DISCOVERY_ENDPOINT; + const client_id = process.env.CLIENT_ID; + const client_secret = process.env.CLIENT_SECRET; + const scope = process.env.SCOPE; + const user = process.env.USER; + let pkce_enabled = process.env.PKCE_ENABLED + + assert(discovery_endpoint, "DISCOVERY_ENDPOINT environment variable is not set."); + assert(client_id, "CLIENT_ID environment variable is not set."); + assert(client_secret, "CLIENT_SECRET environment variable is not set."); + assert(scope, "SCOPE environment variable is not set."); + assert(user, "USER environment variable is not set."); + assert(pkce_enabled, "PKCE_ENABLED environment variable is not set."); + + if (pkce_enabled === "true") { + pkce_enabled = true; + } else if (pkce_enabled === "false") { + pkce_enabled = false; + } else { + console.log("PKCE_ENABLED must be true or false."); + process.exit(1); + } + + await driver.get("http://localhost:3000"); + await populateMetadata(driver, discovery_endpoint); + let access_token = await getAccessToken(driver, client_id, client_secret, scope, pkce_enabled); + await verifyAccessToken(access_token, client_id, scope, user); + console.log("Test completed successfully.") + } catch (error) { + console.log(error.message); + process.exit(1); + } finally { + await driver.quit(); + } +} + +test(); \ No newline at end of file