From eabeb87d6dad89763eb7e972c5a16fb7ae2707e1 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Wed, 29 Oct 2025 09:51:42 -0700
Subject: [PATCH 01/11] OAuth2 Implicit Grant call is working again. Token
Introspection page works with implicit grant results.
---
api/server.js | 77 +++++++++-
client/public/introspection.html | 10 ++
client/src/debugger2.js | 127 ++++++++++++-----
client/src/introspection.js | 127 ++++++++++++-----
local-run-tests.sh | 237 ++++++++++++++++++++++++++-----
local-tests.yml | 63 ++++++--
6 files changed, 518 insertions(+), 123 deletions(-)
diff --git a/api/server.js b/api/server.js
index c2e5551..8407793 100644
--- a/api/server.js
+++ b/api/server.js
@@ -49,7 +49,7 @@ app.get('/healthcheck', function (req, res) {
});
/**
- * System healthcheck
+ * Retrieve Claims Description.
* @route GET /claimdescription
* @group Metadata - Support operations
* @returns {HealthcheckResponse.model} 200 - Claim Description Response
@@ -181,6 +181,81 @@ app.post('/token', (req, res) => {
}
});
+/**
+ * @typedef IntrspectionRequest
+ * @property {string} grant_type.required - The OAuth2 / OIDC Grant / Flow Type
+ * @property {string} client_id.required - The OAuth2 client identifier
+ */
+
+/**
+ * @typedef IntrospectionResponse
+ * @property {string} access_token.required - The OAuth2 Access Token
+ * @property {string} id_token - The OpenID Connect ID Token
+ */
+
+/**
+ * Wrapper around OAuth2 Introspection Endpoint
+ * @route POST /introspection
+ * @group Debugger - Operations for OAuth2/OIDC Debugger
+ * @param {IntrospectionRequest.model} req.body.required - Token Endpoint Request
+ * @returns {IntrospectionResponse.model} 200 - Token Endpoint Response
+ * @returns {Error.model} 400 - Syntax error
+ * @returns {Error.model} 500 - Unexpected error
+ */
+app.post('/introspection', (req, res) => {
+ log.info('Entering app.post for /introspection.');
+ const body = req.body;
+ log.debug('body: ' + JSON.stringify(body));
+ var headers = {
+ "Authorization": req.headers.authorization,
+ "Content-Type": "application/x-www-form-urlencoded"
+ };
+ var introspectionRequestMessage = {
+ token: body.token,
+ token_type_hint: body.token_type_hint
+ }
+ const parameterString = JSON.stringify(introspectionRequestMessage);
+ log.debug("Method: POST");
+ log.debug("URL: " + body.introspectionEndpoint);
+ log.debug("headers: " + JSON.stringify(headers));
+ log.debug("body: " + parameterString);
+ axios({
+ method: 'post',
+ url: body.introspectionEndpoint,
+ headers: headers,
+ data: introspectionRequestMessage,
+ httpsAgent: new (require('https').Agent)({ rejectUnauthorized: true })
+ })
+ .then(function (response) {
+ log.debug('Response from OAuth2 Introspection Endpoint: ' + JSON.stringify(response.data));
+ log.debug('Headers: ' + response.headers);
+ res.status(response.status);
+ res.json(response.data);
+ })
+ .catch(function (error) {
+ log.error('Error from OAuth2 Introspection Endpoint: ' + error);
+ if(!!error.response) {
+ if(!!error.response.status) {
+ log.error("Error Status: " + error.response.status);
+ }
+ if(!!error.response.data) {
+ log.error("Error Response body: " + JSON.stringify(error.response.data));
+ }
+ if(!!error.response.headers) {
+ log.error("Error Response headers: " + error.response.headers);
+ }
+ if (!!error.response) {
+ res.status(error.response.status);
+ res.json(error.response.data);
+ } else {
+ res.status(500);
+ res.json(error.message);
+ }
+ }
+ });
+
+});
+
let options = {
swaggerDefinition: {
info: {
diff --git a/client/public/introspection.html b/client/public/introspection.html
index 49d0b8a..a655f0c 100644
--- a/client/public/introspection.html
+++ b/client/public/introspection.html
@@ -48,6 +48,16 @@
+
OIDC Introspection Token: The OIDC Introspection Token.
diff --git a/client/src/debugger2.js b/client/src/debugger2.js
index 9b676a1..23a54b5 100644
--- a/client/src/debugger2.js
+++ b/client/src/debugger2.js
@@ -40,10 +40,11 @@ function getParameterByName(name, url)
$(document).ready(function() {
log.debug("Entering ready function().");
+
// Call original onload function
onload();
- var sel = $("#authorization_grant_type");
- sel.change(function() {
+
+ $("#authorization_grant_type").change(function() {
log.debug("Entering selection changed function().");
var value = $(this).val();
localStorage.setItem("authorization_grant_type", value);
@@ -56,6 +57,7 @@ $(document).ready(function() {
recalculateRefreshRequestDescription();
log.debug("Leaving selection changed function().");
});
+
var value = $("#authorization_grant_type").val();
resetUI(value);
recalculateRefreshRequestDescription();
@@ -151,7 +153,6 @@ function buildInternalTokenAPIRequestMessage() {
}
var auth_style = getLSBooleanItem("token_post_auth_style");
- log.debug("RCBJ0001: " + auth_style);
var formData = {};
if(grant_type == "authorization_code")
{
@@ -605,6 +606,16 @@ function resetUI(value)
$("#display_authz_request_class").hide();
$("#display_token_request").show();
}
+ if( value == "implicit_grant" &&
+ getParameterByName("redirectFromTokenDetail") != "true")
+ {
+ $("#config_fieldset").hide();
+ $("#config_expand_button").val("Expand");
+ $("#step3").hide();
+ recalculateTokenRequestDescription();
+ recalculateRefreshRequestDescription();
+ }
+
resetErrorDisplays();
$("#yesResourceCheckToken").prop("checked", false);
$("#noResourceCheckToken").prop("checked", true);
@@ -695,19 +706,26 @@ function writeValuesToLocalStorage()
log.debug("Leaving writeValuesToLocalStorage().");
}
-function loadValuesFromLocalStorage()
+// helper function to set the Grant Type menu option.
+function setAuthorizationGrantType()
{
- log.debug("Entering loadValuesFromLocalStorage().");
+ log.debug("Entering setAuthorizationGrantType().");
var authzGrantType = localStorage.getItem("authorization_grant_type");
log.debug("authzGrantType=" + authzGrantType);
if (!!authzGrantType)
{
- $("#authorization_grant_type").val("authorization_grant");
- resetUI("authorization_grant");
- } else {
$("#authorization_grant_type").val(authzGrantType);
- resetUI(authzGrantType);
}
+ resetUI(authzGrantType);
+ log.debug("Entering setAuthorizationGrantType().");
+}
+
+function loadValuesFromLocalStorage()
+{
+ log.debug("Entering loadValuesFromLocalStorage().");
+
+ setAuthorizationGrantType();
+
$("#authorization_endpoint").val(localStorage.getItem("authorization_endpoint"));
$("#token_endpoint").val(localStorage.getItem("token_endpoint"));
@@ -792,12 +810,12 @@ function loadValuesFromLocalStorage()
log.debug("Checking for code. agt=" + agt + ", pathname=" + pathname);
log.debug("fragement: " + parseFragment());
code = parseFragment()["code"];
- if(!!code)
+ if(!!!code)
{
code = "NO_CODE_PRESENTED_IN_EXPECTED_LOCATIONS";
}
log.debug("code=" + code);
- if($("#code").val() == "")
+ if(!!!$("#code").val())
{
log.debug("code not yet set in next form. Doing so now.");
$("#code").val(code);
@@ -807,32 +825,43 @@ function loadValuesFromLocalStorage()
agt == "oidc_implicit_flow" ) &&
pathname == "/debugger2.html") //retrieve access_token for implicit_grant for callback redirect response
{
+ log.debug("Looking for access_token.");
var access_token = getParameterByName("access_token",window.location.href);
log.debug("access_token=" + access_token);
- if(!!access_token)
+ if(!!!access_token)
{
//Check to see if passed in as local anchor (ADFS & Azure Active Directory do this)
+ log.debug("Didn't find token in query parameter. Looking in fragment.");
log.debug("fragement: " + parseFragment());
access_token = parseFragment()["access_token"];
- if(!!access_token)
+ if(!!!access_token)
{
+ log.debug("Didn't find token in fragment.");
access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS";
- }
+ } else {
+ log.debug("Found token in fragment.");
+ }
}
- log.debug("access_token=" + access_token);
var authorization_endpoint_result_html = "
" +
"Authorization Endpoint Results: " +
"" +
"" +
- "access_token " +
- " " +
"
" +
" ";
$("#authorization_endpoint_result").html(DOMPurify.sanitize(authorization_endpoint_result_html));
+ log.debug("RCBJ0001");
+ localStorage.setItem("token_access_token", access_token);
}
if ( agt == "oidc_hybrid_code_id_token_token" &&
pathname == "/debugger2.html") //retrieve access code and id_token that is returned from authorization endpoint.
@@ -1052,6 +1081,41 @@ function recalculateRefreshRequestDescription()
log.debug("Leaving recalculateRefreshRequestDescription().");
}
+function processStateParameter()
+{
+ log.debug("Entering processStateParameter().");
+ // Check if state matches
+ log.debug("Checking on state.");
+ var state = getParameterByName("state");
+ if (!!state) {
+ log.debug("Found state in query parameters: " + state);
+ } else {
+ log.debug("Didn't find state in query parameters, attempting to find fragment.");
+ state = parseFragment()["state"];
+ if(!!state) {
+ log.debug("Found state in fragment.");
+ } else {
+ log.debug("Didn't find state.");
+ }
+ }
+ var storedState = localStorage.getItem("state");
+ // Generate report
+ if ( !!state &&
+ !!storedState &&
+ state == storedState) {
+ log.debug('State matches stored state.');
+ var stateReportHTML = '
State Report ' +
+ '
' + 'State matches: state=' + state + '
';
+ $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
+ } else {
+ log.debug('State does not match: state=' + state + ', storedState=' + storedState);
+ var stateReportHTML = '
State Report ' +
+ '
State does not match: state=' + state + ', storedState=' + storedState + '
';
+ $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
+ }
+ log.debug("Leaving processStateParameter().");
+}
+
function onload() {
log.debug("Entering onload function.");
@@ -1072,24 +1136,8 @@ function onload() {
localStorage.setItem("refresh_refresh_token", "");
}
- // Check if state matches
- log.debug('Checking on state.');
- var state = getParameterByName('state');
- if (!!state) {
- log.debug('Found state: ' + state)
- var storedState = localStorage.getItem('state');
- if ( state == storedState) {
- log.debug('State matches stored state.');
- var stateReportHTML = '
State Report ' +
- '
' + 'State matches: state=' + state + '
';
- $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
- } else {
- log.debug('State does not match: state=' + state + ', storedState=' + storedState);
- var stateReportHTML = '
State Report ' +
- '
State does not match: state=' + state + ', storedState=' + storedState + '
';
- $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
- }
- }
+ processStateParameter();
+
// an error was returned from the authorization endpoint
var errorDescriptionParam = getParameterByName('error_description');
var errorParam = getParameterByName('error');
@@ -1106,6 +1154,12 @@ function onload() {
return;
}
+ // Sets the authorization grant type based upon
+ // what is in local storage, which must be set.
+ // The next call to to resetUI assumes this is set
+ // the way it needs to be.
+ setAuthorizationGrantType();
+
resetUI();
initFields();
generateCustomParametersListUI();
@@ -1795,7 +1849,6 @@ function setHeaderAuthStyleRefreshToken() {
function onClickCopyToken(field) {
log.debug("Entering copyToken().");
var copyText = $(field);
-// copyText.select();
navigator.clipboard.writeText(copyText.val());
log.debug("Leaving copyToken().");
return false;
diff --git a/client/src/introspection.js b/client/src/introspection.js
index a60822c..6d31255 100644
--- a/client/src/introspection.js
+++ b/client/src/introspection.js
@@ -9,6 +9,8 @@ var $ = require("jquery");
var log = bunyan.createLogger({name: 'introspection', level: appconfig.logLevel});
log.info("Log initialized. logLevel=" + log.level());
+var useFrontEnd = false;
+
function getParameterByName(name, url) {
log.debug("Entering getParameterByName().");
if (!url)
@@ -31,47 +33,76 @@ function callIntrospectionEndpoint() {
var introspection_client_secret = document.getElementById("introspection_client_secret").value;
var introspection_bearer_token = document.getElementById("introspection_bearer_token").value;
- var introspectionEndpointCall = $.ajax({
+ var headers = {
+ "Authorization": introspection_authentication_type == "basic_auth" ?
+ "Basic " + btoa(introspection_client_id + ":" + introspection_client_secret) :
+ "Bearer " + introspection_bearer_token,
+ "Content-Type": "application/json"
+ };
+ var body = {
+ token: introspection_token,
+ token_type_hint: introspection_token_type_hint != "" ? introspection_token_type_hint : undefined,
+ }
+ var url_ = "";
+ if(useFrontEnd) {
+ url_ = appconfig.apiUrl + "/introspection";
+ body["introspectionEndpoint"] = introspection_endpoint;
+ } else {
+ url_ = introspection_endpoint;
+ }
+ log.debug("Method: POST");
+ log.debug("URL: " + url_);
+ log.debug("crossDomainn: true");
+ log.debug("body: " + JSON.stringify(body));
+ log.debug("Headers: " + JSON.stringify(headers));
+ $.ajax({
type: "POST",
- url: introspection_endpoint,
+ url: url_,
crossDomain: true,
- headers: {
- "Authorization": introspection_authentication_type == "basic_auth" ?
- "Basic " + btoa(introspection_client_id + ":" + introspection_client_secret) :
- "Bearer " + introspection_bearer_token,
- "Content-Type": "application/x-www-form-urlencoded"
- },
- data: {
- token: introspection_token,
- token_type_hint: introspection_token_type_hint != "" ? introspection_token_type_hint : undefined
- },
- success: function(data, textStatus, request) {
- log.debug('Entering ajax success function for Access Token call.');
- log.debug('Introspection textStatus: ' + JSON.stringify(textStatus));
- log.debug('Introspection Endpoint Response: ' + JSON.stringify(data));
- log.debug('Introspection request: ' + JSON.stringify(request));
- log.debug('Introspection Response Content-Type: ' + introspectionEndpointCall.getResponseHeader("Content-Type"));
- log.debug('Introspection Headers: ' + JSON.stringify(introspectionEndpointCall.getAllResponseHeaders()));
- var responseContentType = introspectionEndpointCall.getResponseHeader("Content-Type");
- if (responseContentType.includes('application/json')) {
- log.debug('plaintext response detected, no signature, no encryption');
- log.debug('Introspection Endpoint Response: ' + JSON.stringify(data, null, 2));
- document.getElementById("introspection_output").value = JSON.stringify(data,null,2);
- } else {
- log.error('Unknown response format.');
- }
- },
- error: function (request, status, error) {
- log.debug("request: " + JSON.stringify(request));
- log.debug("status: " + JSON.stringify(status));
- log.debug("error: " + JSON.stringify(error));
- }
+ headers: headers,
+ data: JSON.stringify(body),
+ success: introspectionSuccess,
+ error: introspectionError
});
-
writeValuesToLocalStorage();
log.debug("Entering callIntrospectionEndpoint().");
}
+function introspectionError(request, status, error) {
+ log.debug("request: " + JSON.stringify(request));
+ log.debug("status: " + JSON.stringify(status));
+ log.debug("error: " + JSON.stringify(error));
+ try {
+ var errorReport = {
+ "request": request,
+ "status": status,
+ "error": error
+ };
+ $("#introspection_output").val(JSON.stringify(errorReport));
+ } catch (e) {
+ log.error("Error occurred while generating error report: " + e.stack);
+ $("#introspection_output").val("Error occurred while generating error report: " + e.stack);
+ }
+}
+
+function introspectionSuccess(data, textStatus, jqXHR) {
+ log.debug('Entering ajax success function for Introspection Endpoint call.');
+ log.debug('Introspection textStatus: ' + JSON.stringify(textStatus));
+ log.debug('Introspection Endpoint Response: ' + JSON.stringify(data));
+ log.debug('Introspection request: ' + JSON.stringify(jqXHR));
+ log.debug('Introspection Response Content-Type: ' + jqXHR.getResponseHeader("Content-Type"));
+ log.debug('Introspection Headers: ' + JSON.stringify(jqXHR.getAllResponseHeaders()));
+ var responseContentType = jqXHR.getResponseHeader("Content-Type");
+ if (responseContentType.includes('application/json')) {
+ log.debug('plaintext response detected, no signature, no encryption');
+ log.debug('Introspection Endpoint Response: ' + JSON.stringify(data, null, 2));
+ $("#introspection_output").val(JSON.stringify(data,null,2));
+ } else {
+ log.error('Unknown response format.');
+ }
+ log.debug("Leaving ajax success function for Introspection Endpoint call.");
+}
+
function loadValuesFromLocalStorage() {
log.debug("Entering loadValuesFromLocalStorage().");
if(localStorage) {
@@ -97,6 +128,8 @@ function loadValuesFromLocalStorage() {
document.getElementById("introspection_client_id").value = localStorage.getItem("introspection_client_id");
document.getElementById("introspection_bearer_token").value = localStorage.getItem("introspection_bearer_token");
+ $("#introspection_initiateFromFrontEnd").prop("checked", getLSBooleanItem("introspection_initiateFromFrontEnd"));
+ $("#introspection_initiateFromBackEnd").prop("checked", getLSBooleanItem("introspection_initiateFromBackEnd"));
log.debug("Leaving loadValuesFromLocalStorage().");
}
@@ -106,6 +139,8 @@ function writeValuesToLocalStorage() {
localStorage.setItem("introspection_endpoint", document.getElementById("introspection_endpoint").value);
localStorage.setItem("introspection_client_id", document.getElementById("introspection_client_id").value);
localStorage.setItem("introspection_bearer_token", document.getElementById("introspection_bearer_token").value);
+ localStorage.setItem("introspection_initiateFromFrontEnd", $("#introspection_initiateFromFrontEnd").is(":checked"));
+ localStorage.setItem("introspection_initiateFromBackEnd", $("#introspection_initiateFromBackEnd").is(":checked"));
}
log.debug("Leaving writeValuesToLocalStorage().");
}
@@ -141,7 +176,27 @@ window.onload = function() {
log.debug("Leaving window.onload() function.");
}
+function setInitiateFromEnd() {
+ log.debug("Entering setInitiateFromEnd().");
+ var frontEndInitiated = $("#introspection_initiateFromFrontEnd").is(":checked");
+ var backEndInitiated = $("#introspection_initiateFromBackEnd").is(":checked");
+ if(frontEndInitiated) {
+ useFrontEnd = true;
+ } else {
+ useFrontEnd = false;
+ }
+ log.debug("frontEndInitiated: " + frontEndInitiated);
+ log.debug("backEndInitiated: " + backEndInitiated);
+ log.debug("Leaving setInitiateFromEnd().");
+}
+
+function getLSBooleanItem(key)
+{
+ return localStorage.getItem(key) === 'true';
+}
+
module.exports = {
callIntrospectionEndpoint,
- onClickToggleConfigurationParameters
-};
\ No newline at end of file
+ onClickToggleConfigurationParameters,
+ setInitiateFromEnd
+};
diff --git a/local-run-tests.sh b/local-run-tests.sh
index 1ff427b..a703dd1 100755
--- a/local-run-tests.sh
+++ b/local-run-tests.sh
@@ -4,6 +4,8 @@ set -x
init()
{
DEBUGGER_BASE_URL=http://localhost:3000
+ KEYCLOAK_BASE_URL=http://localhost:8080
+ KEYCLOAK_LOCALHOST_BASE_URL=http://localhost:8080
CONFIG_FILE=./env/local.js
}
@@ -19,85 +21,227 @@ check_return_code()
init
npm install --prefix tests
-# Install testing dependencies
# Start Docker containers
sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml build
sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml up -d
sleep 60
-
# 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')
+KEYCLOAK_ACCESS_TOKEN=$(curl \
+ -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
then
- echo "Unable to obtain tokens."
- exit 1
+ echo "Failed to obtain access token."
+ exit 1
fi
-echo "KEYCLOAK_ACCESS_TOKEN=${KEYCLOAK_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}'A
+curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d '{"realm": "debugger-testing", "enabled": true}'
check_return_code $?
-for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC
+for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC IMPLICIT
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')
+ KEYCLOAK_ACCESS_TOKEN=$(curl \
+ -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
then
- echo "Unable to obtain tokens."
+ echo "KEYCLOAK_ACCESS_TOKEN is blank."
exit 1
fi
- 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"}}'
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
+ }
+ }'
check_return_code $?
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"}'
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
+ }'
check_return_code $?
;;
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"}}'
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
check_return_code $?
;;
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"}}'
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ ;;
+ IMPLICIT)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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,
+ "implicitFlowEnabled": true,
+ "directAccessGrantsEnabled": false,
+ "clientAuthenticatorType": null,
+ "frontchannelLogout": true,
+ "redirectUris": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
check_return_code $?
;;
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')
- if [ -z "${CLIENT_ID}" ] ||
- [ -z "${CLIENT_CLIENTID}" ] ||
- [ -z "${CLIENT_SECRET}" ] ||
- [ -z "${SCOPE_ID}" ];
- then
- echo "Variable is blank."
- exit 1
- fi
- curl -X PUT "http://localhost:8080/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
+ CLIENT_ID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].id')
+ CLIENT_CLIENTID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].clientId')
+ CLIENT_SECRET=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].secret')
+ SCOPE_ID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id')
+ SCOPE_NAME=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .name')
+ curl \
+ -X PUT \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
check_return_code $?
- 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')
- if [ -z "${USER_ID}" ];
+ USER_ID=$(curl \
+ -X POST \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
+ if [ -z "${CLIENT_ID}" ] || \
+ [ -z "${CLIENT_CLIENTID}" ] || \
+ [ -z "${CLIENT_SECRET}" ] || \
+ [ -z "${SCOPE_ID}" ] || \
+ [ -z "${SCOPE_NAME} ] || \
+ [ -z "${USER_ID} ];
then
- echo "USER_ID is blank."
+ echo "Required variable is blank."
exit 1
- fi
- 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}'
+ fi
+ curl \
+ -X PUT \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/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
+ }'
check_return_code $?
- declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration"
+ declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/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
-node --version
# Test client credentials flow
DISCOVERY_ENDPOINT=${CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT} \
CLIENT_ID=${CLIENT_CREDENTIALS_CLIENT_ID} \
@@ -109,6 +253,13 @@ check_return_code $?
# Test authorization code flow
for PKCE_ENABLED in true false
do
+ echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
+ echo "CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
+ echo "SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}"
+ echo "USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
# Confidential client
DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT} \
CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID} \
@@ -116,9 +267,16 @@ do
SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE} \
USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER} \
PKCE_ENABLED=${PKCE_ENABLED} \
- node tests/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}" --browser
+ node tests/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
+ echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
+ echo "CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
+ echo "SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE}"
+ echo "USER=${AUTHORIZATION_CODE_PUBLIC_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
# Public client
DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT} \
CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID} \
@@ -130,4 +288,11 @@ do
check_return_code $?
done
+echo DISCOVERY_ENDPOINT=${IMPLICIT_DISCOVERY_ENDPOINT}
+echo CLIENT_ID=${IMPLICIT_CLIENT_ID}
+echo CLIENT_SECRET=${IMPLICIT_CLIENT_SECRET}
+echo SCOPE=${IMPLICIT_SCOPE}
+echo USER=${IMPLICIT_USER}
+
+node --version
exit 0
diff --git a/local-tests.yml b/local-tests.yml
index 3b16d98..d9c43ea 100644
--- a/local-tests.yml
+++ b/local-tests.yml
@@ -1,18 +1,25 @@
version: '3'
services:
postgres:
- image: postgres:15
- network_mode: "host"
+ image: postgres:17
+ hostname: postgres
+ network_mode: "host"
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
POSTGRES_DB: keycloak
-
+ healthcheck:
+ test: pg_isready
+ interval: 10s
+ timeout: 60s
+ retries: 5
+ start_period: 45s
keycloak:
image: quay.io/keycloak/keycloak:26.1.4
command: ["start-dev"]
+ hostname: keycloak
network_mode: "host"
environment:
KC_DB: postgres
@@ -21,16 +28,31 @@ services:
KC_DB_PASSWORD: keycloak
KEYCLOAK_ADMIN: keycloak
KEYCLOAK_ADMIN_PASSWORD: keycloak
+ KC_HEALTH_ENABLED: "true"
+ healthcheck:
+ test: [
+ "CMD-SHELL",
+ "exec 3<>/dev/tcp/localhost/9000; \
+ echo -en 'GET /health/ready' >&3; \
+ # Give the server a moment to respond, then search for 'UP'
+ if timeout 3 cat <&3 | grep -m 1 'UP'; then \
+ exec 3<&-; exec 3>&-; exit 0; \
+ else \
+ exec 3<&-; exec 3>&-; exit 1; \
+ fi"
+ ]
+ interval: 10s
+ timeout: 60s
+ retries: 5
+ start_period: 30s
depends_on:
- - postgres
-
+ postgres:
+ condition: service_healthy
api:
container_name: api
+ hostname: api
image: rcbj/api
- environment:
- - HOST=0.0.0.0
- - PORT=4000
- - LOG_LEVEL=debug
+ network_mode: "host"
environment:
- CONFIG_FILE=./env/local.js
build:
@@ -38,13 +60,22 @@ services:
dockerfile: api/Dockerfile
args:
CONFIG_FILE: ${CONFIG_FILE}
- network_mode: "host"
+ depends_on:
+ keycloak:
+ condition: service_healthy
+ healthcheck:
+ test: curl -X GET http://localhost:4000
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
depends_on:
- keycloak
-
client:
container_name: client
+ hostname: client
image: rcbj/client
+ network_mode: "host"
environment:
- CONFIG_FILE=./env/local.js
build:
@@ -52,9 +83,15 @@ services:
dockerfile: client/Dockerfile
args:
CONFIG_FILE: ${CONFIG_FILE}
- network_mode: "host"
+ healthcheck:
+ test: curl -X GET http://localhost:3000
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
depends_on:
- - api
+ keycloak:
+ condition: service_healthy
volumes:
postgres_data:
From 45e867fb6c836638375d3f3824ee670e8846034d Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Thu, 30 Oct 2025 04:40:18 -0700
Subject: [PATCH 02/11] All functionality working with the OAuth2 Authorization
Grant.
---
client/public/debugger2.html | 6 +-
client/public/introspection.html | 4 +-
client/src/debugger2.js | 162 ++++++++++++++++++++-----------
client/src/introspection.js | 10 +-
4 files changed, 120 insertions(+), 62 deletions(-)
diff --git a/client/public/debugger2.html b/client/public/debugger2.html
index 5a6dfa4..b0230c0 100644
--- a/client/public/debugger2.html
+++ b/client/public/debugger2.html
@@ -166,7 +166,7 @@
Exchange Authorization Code for Access Token
-
+
@@ -452,7 +452,7 @@
-
+
@@ -494,7 +494,7 @@
-
+
diff --git a/client/public/introspection.html b/client/public/introspection.html
index a655f0c..83c6991 100644
--- a/client/public/introspection.html
+++ b/client/public/introspection.html
@@ -54,8 +54,8 @@
Front
- Back
-
+ Back
+
diff --git a/client/src/debugger2.js b/client/src/debugger2.js
index 23a54b5..2d225f3 100644
--- a/client/src/debugger2.js
+++ b/client/src/debugger2.js
@@ -1,7 +1,3 @@
-// File: debugger2_js.js
-// Author: Robert C. Broeckelmann Jr.
-// Date: 06/15/2017
-//
const appconfig = require(process.env.CONFIG_FILE);
const bunyan = require("bunyan");
const DOMPurify = require("dompurify");
@@ -62,34 +58,34 @@ $(document).ready(function() {
resetUI(value);
recalculateRefreshRequestDescription();
- $("#logout_btn").click(function() {
- log.debug("Logout link clicked.");
- var nameValuePairs = {};
-
- $('#logout_fieldset input.q').each(function() {
- var className = $(this).attr('name');
- var value = $(this).val();
- if (value!=""){
- nameValuePairs[className] = value;
- }
- });
- log.debug(nameValuePairs); // Log the name-value pairs
- var queryString = $.param(nameValuePairs);
+ $(".token_btn").click(tokenButtonClick);
+ $(".refresh_btn").click(refreshButtonClick);
- log.debug(queryString); // Log the query string
- var logoutUrl = DOMPurify.sanitize($("#logout_end_session_endpoint").val()) + "?" + DOMPurify.sanitize(queryString);
+ log.debug("Leaving token submit button clicked function.");
+});
- clearLocalStorage();
- window.location.href = logoutUrl;
+function logoutButtonClick() {
+ log.debug("Logout link clicked.");
+ var nameValuePairs = {};
- return false;
+ $('#logout_fieldset input.q').each(function() {
+ var className = $(this).attr('name');
+ var value = $(this).val();
+ if (value!=""){
+ nameValuePairs[className] = value;
+ }
});
+ log.debug(nameValuePairs); // Log the name-value pairs
+ var queryString = $.param(nameValuePairs);
- $(".token_btn").click(tokenButtonClick);
- $(".refresh_btn").click(refreshButtonClick);
+ log.debug(queryString); // Log the query string
+ var logoutUrl = DOMPurify.sanitize($("#logout_end_session_endpoint").val()) + "?" + DOMPurify.sanitize(queryString);
- log.debug("Leaving token submit button clicked function.");
-});
+ clearLocalStorage();
+ window.location.href = logoutUrl;
+
+ return false;
+};
function tokenButtonClick() {
log.debug("Entering token Submit button clicked function.");
@@ -241,6 +237,7 @@ function successfulInternalTokenAPICall(data, textStatus, request)
if(displayOpenIDConnectArtifacts == true)
{
// Display OAuth2/OIDC Artifacts
+ log.debug("RCBJ0003");
token_endpoint_result_html = "" +
"Token Endpoint Results: " +
"" +
@@ -292,6 +289,7 @@ function successfulInternalTokenAPICall(data, textStatus, request)
localStorage.setItem("token_id_token", data.id_token);
} else {
log.debug("Displaying Access Token. No OIDC ID Token: data.access_token=" + data.access_token);
+ log.debug("RCBJ0004");
token_endpoint_result_html = "" +
"Token Endpoint Results: " +
"" +
@@ -615,7 +613,14 @@ function resetUI(value)
recalculateTokenRequestDescription();
recalculateRefreshRequestDescription();
}
-
+ if( value == "implicit_grant" &&
+ getParameterByName("redirectFromTokenDetail") == "true")
+ {
+ $("#config_fieldset").hide();
+ $("#config_expand_button").val("Expand");
+ $("#step3").hide();
+ }
+
resetErrorDisplays();
$("#yesResourceCheckToken").prop("checked", false);
$("#noResourceCheckToken").prop("checked", true);
@@ -796,7 +801,12 @@ function loadValuesFromLocalStorage()
$("#token_pkce_code_method").val(localStorage.getItem("PKCE_code_challenge_method"));
}
usePKCERFC();
+ log.debug("Leaving loadValuesFromLocalStorage().");
+}
+function recreateUniqueGrantFlowElements()
+{
+ log.debug("Entering recreateUniqueGrantFlowElements().");
var agt = $("#authorization_grant_type").val();
var pathname = window.location.pathname;
log.debug("agt=" + agt);
@@ -821,9 +831,8 @@ function loadValuesFromLocalStorage()
$("#code").val(code);
}
}
- if ( (agt == "implicit_grant" ||
- agt == "oidc_implicit_flow" ) &&
- pathname == "/debugger2.html") //retrieve access_token for implicit_grant for callback redirect response
+ if ( agt == "implicit_grant" ||
+ agt == "oidc_implicit_flow")
{
log.debug("Looking for access_token.");
var access_token = getParameterByName("access_token",window.location.href);
@@ -836,12 +845,21 @@ function loadValuesFromLocalStorage()
access_token = parseFragment()["access_token"];
if(!!!access_token)
{
- log.debug("Didn't find token in fragment.");
- access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS";
+ log.debug("Didn't find token in fragment. Checking to see if there is a saved token in local storage.");
+ access_token = localStorage.getItem("token_access_token");
+ if(!!!access_token)
+ {
+ log.debug("Didn't find token in local storage. No access_token found.");
+ access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS(IMPLICIT_GRANT||OIDC_IMPLICIT_FLOW)";
+ } else {
+ log.debug("Found access_token in local storage.");
+ }
} else {
log.debug("Found token in fragment.");
}
- }
+ } else {
+ log.debug("Found token in query parameter.");
+ }
var authorization_endpoint_result_html = "" +
"Authorization Endpoint Results: " +
"" +
@@ -860,7 +878,6 @@ function loadValuesFromLocalStorage()
"
" +
" ";
$("#authorization_endpoint_result").html(DOMPurify.sanitize(authorization_endpoint_result_html));
- log.debug("RCBJ0001");
localStorage.setItem("token_access_token", access_token);
}
if ( agt == "oidc_hybrid_code_id_token_token" &&
@@ -870,7 +887,7 @@ function loadValuesFromLocalStorage()
access_token = parseFragment()["access_token"];
if(!access_token)
{
- access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS";
+ access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS(oidc_hybrid_code_id_token_token)";
}
log.debug("access_token=" + access_token);
log.debug("fragement: " + parseFragment());
@@ -917,7 +934,7 @@ function loadValuesFromLocalStorage()
access_token = parseFragment()["access_token"];
if(!access_token)
{
- access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS";
+ access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS(oidc_hybrid_code_token)";
}
log.debug("access_token=" + access_token);
$("#authorization_endpoint_result").html(DOMPurify.sanitize("" +
@@ -964,12 +981,31 @@ function loadValuesFromLocalStorage()
var error = getParameterByName("error",window.location.href);
var authzGrantType = $("#authorization_grant_type").val();
if( pathname == "/debugger2.html" &&
- (authzGrantType == "authorization_grant" || authzGrantType == "implicit_grant" || authzGrantType == "oidc_hybrid_code_id_token") &&
- (!!error))
+ ( authzGrantType == "authorization_grant" ||
+ authzGrantType == "implicit_grant" ||
+ authzGrantType == "oidc_hybrid_code_id_token") &&
+ (!!error))
{
- $("#display_authz_error_class").html(DOMPurify.sanitize("Authorization Endpoint Error "));
+ error_html = "" +
+ "Authorization Endpoint Error " +
+ "" +
+ "" +
+ "" +
+ "" +
+ "Error " +
+ " " +
+ "" +
+ "" +
+ error +
+ " "
+ " " +
+ " " +
+ "
" +
+ " " +
+ " ";
+ $("#display_authz_error_class").html(DOMPurify.sanitize(error_html));
}
- log.debug("Leaving loadValuesFromLocalStorage().");
+ log.debug("Entering recreateUniqueGrantFlowElements().");
}
function recalculateTokenRequestDescription()
@@ -1087,31 +1123,36 @@ function processStateParameter()
// Check if state matches
log.debug("Checking on state.");
var state = getParameterByName("state");
+ var stateParameterFound = false;
if (!!state) {
log.debug("Found state in query parameters: " + state);
+ stateParameterFound = true;
} else {
log.debug("Didn't find state in query parameters, attempting to find fragment.");
state = parseFragment()["state"];
if(!!state) {
log.debug("Found state in fragment.");
+ stateParameterFound = true
} else {
log.debug("Didn't find state.");
}
}
var storedState = localStorage.getItem("state");
// Generate report
- if ( !!state &&
- !!storedState &&
- state == storedState) {
- log.debug('State matches stored state.');
- var stateReportHTML = 'State Report ' +
- '' + 'State matches: state=' + state + '
';
- $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
- } else {
- log.debug('State does not match: state=' + state + ', storedState=' + storedState);
- var stateReportHTML = 'State Report ' +
- 'State does not match: state=' + state + ', storedState=' + storedState + '
';
- $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
+ if(stateParameterFound) {
+ if ( !!state &&
+ !!storedState &&
+ state == storedState) {
+ log.debug('State matches stored state.');
+ var stateReportHTML = 'State Report ' +
+ '' + 'State matches: state=' + state + '
';
+ $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
+ } else {
+ log.debug('State does not match: state=' + state + ', storedState=' + storedState);
+ var stateReportHTML = 'State Report ' +
+ 'State does not match: state=' + state + ', storedState=' + storedState + '
';
+ $("#state-status").html(DOMPurify.sanitize(stateReportHTML));
+ }
}
log.debug("Leaving processStateParameter().");
}
@@ -1126,8 +1167,11 @@ function onload() {
$("#password-form-group1").hide();
$("#password-form-group2").hide();
+ // If we are coming back from the Token Detail Page clear all saved tokens.
+ // It will be reset.
if(getParameterByName("redirectFromTokenDetail") != "true") {
// Clear all token values.
+ log.debug("Detected page load for new grant/flow workflow. Clearing all existing tokens.");
localStorage.setItem("token_access_token", "");
localStorage.setItem("token_id_token", "");
localStorage.setItem("token_refresh_token", "");
@@ -1142,7 +1186,8 @@ function onload() {
var errorDescriptionParam = getParameterByName('error_description');
var errorParam = getParameterByName('error');
log.debug('errorDescriptionParam=' + errorDescriptionParam + ', errorParam=' + errorParam);
- if (errorDescriptionParam || errorParam) {
+ if (errorDescriptionParam ||
+ errorParam) {
$('#step0').hide();
$('#step3').hide();
$('#step4').hide();
@@ -1168,6 +1213,7 @@ function onload() {
$("#customTokenParametersCheck-no").on("click", recalculateTokenRequestDescription);
loadValuesFromLocalStorage();
+ recreateUniqueGrantFlowElements();
recalculateAuthorizationErrorDescription();
recalculateTokenRequestDescription();
recalculateRefreshRequestDescription();
@@ -1218,7 +1264,10 @@ function onload() {
displayTokenCustomParametersCheck();
- if(getParameterByName("redirectFromTokenDetail") == "true") {
+ if( getParameterByName("redirectFromTokenDetail") == "true" &&
+ ( value != "implicit_grant" &&
+ value != "oidc_implicit_grant"))
+ {
log.debug('Detected redirect back from token detail page.');
$("#step3").hide();
recreateTokenDisplay();
@@ -1565,6 +1614,7 @@ function recreateTokenDisplay()
{
log.debug("Displaying full OIDC Token results.");
// Display OAuth2/OIDC Artifacts
+ log.debug("RCBJ0001");
token_endpoint_result_html = "" +
"Token Endpoint Results: " +
"" +
@@ -1615,6 +1665,7 @@ function recreateTokenDisplay()
} else {
log.debug("Logging access_token only.");
+ log.debug("RCBJ0002");
token_endpoint_result_html = "" +
"Token Endpoint Results: " +
"" +
@@ -1914,5 +1965,6 @@ module.exports = {
setHeaderAuthStyleRefreshToken,
onClickCopyToken,
setInitiateFromEnd,
- setInitiateRefreshFromEnd
+ setInitiateRefreshFromEnd,
+ logoutButtonClick
};
diff --git a/client/src/introspection.js b/client/src/introspection.js
index 6d31255..7001e12 100644
--- a/client/src/introspection.js
+++ b/client/src/introspection.js
@@ -173,11 +173,17 @@ window.onload = function() {
log.debug("Entering window.onload() function.");
loadValuesFromLocalStorage();
$("#introspection_authentication_type").trigger("change");
+ var frontEndInitiated = $("#introspection_initiateFromFrontEnd").is(":checked");
+ if(frontEndInitiated) {
+ useFrontEnd = true;
+ } else {
+ useFrontEnd = false;
+ }
log.debug("Leaving window.onload() function.");
}
-function setInitiateFromEnd() {
- log.debug("Entering setInitiateFromEnd().");
+function setInitiateFromEnd(which_end) {
+ log.debug("Entering setInitiateFromEnd(). which_end=" + which_end);
var frontEndInitiated = $("#introspection_initiateFromFrontEnd").is(":checked");
var backEndInitiated = $("#introspection_initiateFromBackEnd").is(":checked");
if(frontEndInitiated) {
From db3d5f44f7f086448ebec5c8f4a4899a29497ba8 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Thu, 30 Oct 2025 11:00:38 -0700
Subject: [PATCH 03/11] OAuth2 Implicity Grant automated test.
---
common/common.sh | 303 ++++++++++++++++++++++++++++++++++++++
local-run-tests.sh | 305 ++++-----------------------------------
tests/oauth2_implicit.js | 228 +++++++++++++++++++++++++++++
3 files changed, 558 insertions(+), 278 deletions(-)
create mode 100755 common/common.sh
create mode 100644 tests/oauth2_implicit.js
diff --git a/common/common.sh b/common/common.sh
new file mode 100755
index 0000000..38c3736
--- /dev/null
+++ b/common/common.sh
@@ -0,0 +1,303 @@
+#!/bin/bash
+set -x
+
+check_return_code()
+{
+ rc=$1
+ if [ $rc -ne 0 ];
+ then
+ echo "Non-zero return code. Exiting."
+ exit 1
+ fi
+}
+
+configureKeycloak()
+{
+ echo "Entering configureKeycloak()."
+ # Configure Keycloak
+ KEYCLOAK_ACCESS_TOKEN=$(curl \
+ -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
+ if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
+ then
+ echo "Failed to obtain access token."
+ exit 1
+ fi
+
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d '{"realm": "debugger-testing", "enabled": true}'
+ check_return_code $?
+
+ for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC IMPLICIT
+ do
+ FLOW_NAME=$(echo ${FLOW_VARIABLE} | tr '[:upper:]' '[:lower:]' | tr '_' '-')
+
+ KEYCLOAK_ACCESS_TOKEN=$(curl \
+ -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
+ if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
+ then
+ echo "KEYCLOAK_ACCESS_TOKEN is blank."
+ exit 1
+ fi
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
+ }
+ }'
+ check_return_code $?
+ case "${FLOW_VARIABLE}" in
+ CLIENT_CREDENTIALS)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
+ }'
+ check_return_code $?
+ ;;
+ AUTHORIZATION_CODE_CONFIDENTIAL)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ check_return_code $?
+ ;;
+ AUTHORIZATION_CODE_PUBLIC)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ ;;
+ IMPLICIT)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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,
+ "implicitFlowEnabled": true,
+ "directAccessGrantsEnabled": false,
+ "clientAuthenticatorType": null,
+ "frontchannelLogout": true,
+ "redirectUris": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ check_return_code $?
+ ;;
+ esac
+
+ CLIENT_ID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].id')
+ CLIENT_CLIENTID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].clientId')
+ CLIENT_SECRET=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[0].secret')
+ SCOPE_ID=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id')
+ SCOPE_NAME=$(curl \
+ -X GET \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
+ | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .name')
+ curl \
+ -X PUT \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" \
+ -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
+ check_return_code $?
+ USER_ID=$(curl \
+ -X POST \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
+ if [ -z "${CLIENT_ID}" ] || \
+ [ -z "${CLIENT_CLIENTID}" ] || \
+ [ -z "${CLIENT_SECRET}" ] || \
+ [ -z "${SCOPE_ID}" ] || \
+ [ -z "${SCOPE_NAME} ] || \
+ [ -z "${USER_ID} ];
+ then
+ echo "Required variable is blank."
+ exit 1
+ fi
+ curl \
+ -X PUT \
+ "${KEYCLOAK_LOCALHOST_BASE_URL}/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
+ }'
+ check_return_code $?
+
+ declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/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
+ echo "Leaving configureKeycloak()."
+ env
+ exit 1
+}
+
+runTests()
+{
+ echo "Entering runTests()."
+ env
+ # Test client credentials flow
+ 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 --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+
+ # Test authorization code flow
+ for PKCE_ENABLED in true false
+ do
+ echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
+ echo "CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
+ echo "SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}"
+ echo "USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
+ # 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 --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+
+ echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
+ echo "CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
+ echo "SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE}"
+ echo "USER=${AUTHORIZATION_CODE_PUBLIC_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
+ # 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 --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+ done
+
+ # OAuth2 Implicit Grant
+ DISCOVERY_ENDPOINT=${IMPLICIT_DISCOVERY_ENDPOINT} \
+ CLIENT_ID=${IMPLICIT_CLIENT_ID} \
+ SCOPE=${IMPLICIT_SCOPE} \
+ USER=${IMPLICIT_USER} \
+ node tests/oauth2_implicit.js --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+
+ echo "Leaving runTests()."
+}
+
+execute()
+{
+ configureKeycloak
+ check_return_code $?
+ runTests
+ check_return_code $?
+}
diff --git a/local-run-tests.sh b/local-run-tests.sh
index a703dd1..d2e2f13 100755
--- a/local-run-tests.sh
+++ b/local-run-tests.sh
@@ -1,298 +1,47 @@
#!/bin/bash
set -x
-
+#
+# This script runs tests locally.
+#
init()
{
DEBUGGER_BASE_URL=http://localhost:3000
KEYCLOAK_BASE_URL=http://localhost:8080
KEYCLOAK_LOCALHOST_BASE_URL=http://localhost:8080
CONFIG_FILE=./env/local.js
-}
-
-check_return_code()
-{
- rc=$1
- if [ $rc -ne 0 ];
+ CURRENT_DIR=`echo "$(dirname "$(realpath "$0")")"`
+ COMMON_SH=${CURRENT_DIR}/common/common.sh
+ if [ -r "${COMMON_SH}" ];
then
- echo "Non-zero return code. Exiting."
+ . ${COMMON_SH}
+ else
+ echo "Cannot find ${COMMON_SH}."
exit 1
fi
}
-init
-npm install --prefix tests
+prepTestEnv()
+{
+ npm install --prefix tests
+}
-# Start Docker containers
-sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml build
-sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml up -d
+startDocker()
+{
+ # Start Docker containers
+ sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml build
+ sudo CONFIG_FILE=./env/local.js docker-compose -f local-tests.yml up -d
+}
+init
+check_return_code $?
+prepTestEnv
+check_return_code $?
+startDocker
+check_return_code $?
sleep 60
-# Configure Keycloak
-KEYCLOAK_ACCESS_TOKEN=$(curl \
- -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
-if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
-then
- echo "Failed to obtain access token."
- exit 1
-fi
-
-curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- -H "Content-Type: application/json" \
- -d '{"realm": "debugger-testing", "enabled": true}'
check_return_code $?
-
-for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC IMPLICIT
-do
- FLOW_NAME=$(echo ${FLOW_VARIABLE} | tr '[:upper:]' '[:lower:]' | tr '_' '-')
-
- KEYCLOAK_ACCESS_TOKEN=$(curl \
- -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
- if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
- then
- echo "KEYCLOAK_ACCESS_TOKEN is blank."
- exit 1
- fi
- curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
- }
- }'
- check_return_code $?
- case "${FLOW_VARIABLE}" in
- CLIENT_CREDENTIALS)
- curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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"
- }'
- check_return_code $?
- ;;
- AUTHORIZATION_CODE_CONFIDENTIAL)
- curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
- "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
- "attributes": {
- "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
- "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
- }
- }'
- check_return_code $?
- ;;
- AUTHORIZATION_CODE_PUBLIC)
- curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
- "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
- "attributes": {
- "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
- "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
- }
- }'
- ;;
- IMPLICIT)
- curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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,
- "implicitFlowEnabled": true,
- "directAccessGrantsEnabled": false,
- "clientAuthenticatorType": null,
- "frontchannelLogout": true,
- "redirectUris": ["'${DEBUGGER_BASE_URL}/callback'"],
- "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
- "attributes": {
- "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
- "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
- }
- }'
- check_return_code $?
- ;;
- esac
-
- CLIENT_ID=$(curl \
- -X GET \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].id')
- CLIENT_CLIENTID=$(curl \
- -X GET \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].clientId')
- CLIENT_SECRET=$(curl \
- -X GET \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].secret')
- SCOPE_ID=$(curl \
- -X GET \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id')
- SCOPE_NAME=$(curl \
- -X GET \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/client-scopes" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .name')
- curl \
- -X PUT \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
- check_return_code $?
- USER_ID=$(curl \
- -X POST \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/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')
- if [ -z "${CLIENT_ID}" ] || \
- [ -z "${CLIENT_CLIENTID}" ] || \
- [ -z "${CLIENT_SECRET}" ] || \
- [ -z "${SCOPE_ID}" ] || \
- [ -z "${SCOPE_NAME} ] || \
- [ -z "${USER_ID} ];
- then
- echo "Required variable is blank."
- exit 1
- fi
- curl \
- -X PUT \
- "${KEYCLOAK_LOCALHOST_BASE_URL}/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
- }'
- check_return_code $?
-
- declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/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=${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 --url "${DEBUGGER_BASE_URL}"
+execute
check_return_code $?
-
-# Test authorization code flow
-for PKCE_ENABLED in true false
-do
- echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
- echo "CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
- echo "CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
- echo "SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}"
- echo "USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER}"
- echo "PKCE_ENABLED=${PKCE_ENABLED}"
-
- # 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 --url "${DEBUGGER_BASE_URL}"
- check_return_code $?
-
- echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
- echo "CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
- echo "CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
- echo "SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE}"
- echo "USER=${AUTHORIZATION_CODE_PUBLIC_USER}"
- echo "PKCE_ENABLED=${PKCE_ENABLED}"
-
- # 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 --url "${DEBUGGER_BASE_URL}"
- check_return_code $?
-done
-
-echo DISCOVERY_ENDPOINT=${IMPLICIT_DISCOVERY_ENDPOINT}
-echo CLIENT_ID=${IMPLICIT_CLIENT_ID}
-echo CLIENT_SECRET=${IMPLICIT_CLIENT_SECRET}
-echo SCOPE=${IMPLICIT_SCOPE}
-echo USER=${IMPLICIT_USER}
-
node --version
+check_return_code $?
exit 0
diff --git a/tests/oauth2_implicit.js b/tests/oauth2_implicit.js
new file mode 100644
index 0000000..eeaf1cc
--- /dev/null
+++ b/tests/oauth2_implicit.js
@@ -0,0 +1,228 @@
+const { Builder, By, until, logging } = require("selenium-webdriver");
+const { Select } = require('selenium-webdriver/lib/select');
+const chrome = require("selenium-webdriver/chrome");
+const jwt = require("jsonwebtoken");
+const assert = require("assert");
+const { Command, Option } = require('commander');
+
+var baseUrl = "http://localhost:3000"
+var headless = true;
+
+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, scope) {
+ console.log("Entering getAccessToken().");
+ console.log("Find authorization_grant_type.");
+ authorization_grant_type = By.id("authorization_grant_type");
+ console.log("Find authz_expand_button");
+ authz_expand_button = By.id("authz_expand_button");
+ console.log("Find client_id.");
+ client_id_ = By.id("client_id");
+ console.log("Find scope.");
+ scope_ = By.id("scope");
+ console.log("find token_client_id.");
+ token_client_id = By.id("token_client_id");
+ console.log("Find token_scope.");
+ token_scope = By.id("token_scope");
+ console.log("Find btn_authorize.");
+ btn_authorize = By.css("input[type=\"submit\"][value=\"Authorize\"]");
+ console.log("Find username.");
+ keycloak_username = By.id("username");
+ console.log("Find password.");
+ keycloak_password = By.id("password");
+ console.log("Find kc-login");
+ keycloak_kc_login = By.id("kc-login");
+ console.log("Find token_access_token.");
+ token_access_token = By.id("token_access_token");
+ console.log("Find display_token_error_form_texarea1.");
+ display_token_error_form_textarea1 = By.id("display_token_error_form_textarea1");
+
+ // Select OAuth2 Implicit Grant
+ console.log("Set authorization_grant_type to OAuth2 Implicit Grant.");
+ await new Select(await driver.findElement(authorization_grant_type)).selectByVisibleText('OAuth2 Implicit Grant');
+
+ console.log("Find authz_expand_button.");
+ await driver.wait(until.elementLocated(authz_expand_button), 10000);
+ console.log("Waiting for authz_expand_button to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000);
+ console.log("Click authz_expand_button.");
+ await driver.findElement(authz_expand_button).click();
+ console.log("Locate client_id_.");
+ await driver.wait(until.elementLocated(client_id_), 10000);
+ console.log("Find client_id_.");
+ await driver.wait(until.elementIsVisible(driver.findElement(client_id_)), 10000);
+
+ // Submit credentials
+ console.log("Clear client_id_.");
+ await driver.findElement(client_id_).clear();
+ console.log("Set client_id value.");
+ await driver.findElement(client_id_).sendKeys(client_id);
+ console.log("Clear scope_.");
+ await driver.findElement(scope_).clear();
+ console.log("Set scope value.");
+ await driver.findElement(scope_).sendKeys(scope);
+ console.log("Find token_redirect_uri.");
+ redirect_uri = By.id("redirect_uri");
+ console.log("Clear redirect_uri.");
+ await driver.findElement(redirect_uri).clear();
+ console.log("Set redirect_uri value: redirect_uri=" + redirect_uri + ", redirect_uri=" + baseUrl + "/callback");
+ await driver.findElement(redirect_uri).sendKeys(baseUrl + "/callback");
+ console.log("Click btn_authorize button.");
+ await driver.findElement(btn_authorize).click();
+
+ // Login to Keycloak
+ try {
+ console.log("Wait for keycloak_username.");
+ await driver.wait(until.elementLocated(keycloak_username), 10000);
+ console.log("Wait for keycloak_username to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(keycloak_username)), 10000);
+ } catch (error) {
+ console.log("Unable to log into keycloak.");
+ 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());
+ }
+
+ console.log("Clear keycloak_username.");
+ await driver.findElement(keycloak_username).clear();
+ console.log("Set keycloak_username value.");
+ await driver.findElement(keycloak_username).sendKeys(client_id);
+ console.log("Clear keycloak_password.");
+ await driver.findElement(keycloak_password).clear();
+ console.log("Set client_id value.");
+ await driver.findElement(keycloak_password).sendKeys(client_id);
+ console.log("Click keycloak_kc_login button.");
+ await driver.findElement(keycloak_kc_login).click();
+
+
+ // Get access token result
+ async function waitForVisibility(element) {
+ console.log("Waiting for " + element);
+ await driver.wait(until.elementLocated(element), 10000);
+ console.log("Waiting for " + element + "is visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(element)), 10000);
+ console.log("Returning " + element);
+ return element;
+ }
+
+ let visibleAccessTokenElement = await Promise.any([
+ waitForVisibility(token_access_token),
+ waitForVisibility(display_token_error_form_textarea1)
+ ]);
+
+ console.log("Begin returning token.");
+ 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();
+ if(headless) {
+ options.addArguments("--headless");
+ }
+ options.addArguments("--no-sandbox");
+ const loggingPrefs = new logging.Preferences();
+ loggingPrefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
+
+ const driver = await new Builder()
+ .forBrowser("chrome")
+ .setChromeOptions(options)
+ .setLoggingPrefs(loggingPrefs)
+ .build();
+
+ try {
+ const discovery_endpoint = process.env.DISCOVERY_ENDPOINT;
+ const client_id = process.env.CLIENT_ID;
+ const scope = process.env.SCOPE;
+ const user = process.env.USER;
+
+ assert(discovery_endpoint, "DISCOVERY_ENDPOINT environment variable is not set.");
+ assert(client_id, "CLIENT_ID environment variable is not set.");
+ assert(scope, "SCOPE environment variable is not set.");
+ assert(user, "USER environment variable is not set.");
+
+ console.log("Kicking off test.");
+ await driver.get(baseUrl);
+ console.log("Calling populateMetadata().");
+ await populateMetadata(driver, discovery_endpoint);
+ console.log("Calling getAccessToken().");
+ let access_token = await getAccessToken(driver, client_id, scope);
+ console.log("Access token: " + access_token);
+ console.log("Calling verifyAccessToken().");
+ 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();
+ }
+}
+
+const program = new Command();
+program
+ .name('oauth_authorization_code')
+ .description("Run test.")
+ .addOption(
+ new Option(
+ "-u, --url ",
+ "Set base URL.")
+ .makeOptionMandatory()
+ )
+ .addOption(
+ new Option(
+ "-b, --browser",
+ "Display browser (only works within device).")
+ )
+ .action((options) => {
+ if(!!options.url) {
+ console.log("Setting url to " + options.url);
+ baseUrl = options.url;
+ }
+ if(!!options.browser) {
+ console.log("Using browser. headless = false.");
+ headless = false;
+ }
+ });
+
+program.parse(process.argv).opts();
+
+test();
From d404362b6c20f0c938ec6cf3ffbb397fd0e1c68d Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Fri, 31 Oct 2025 06:34:41 -0700
Subject: [PATCH 04/11] Docker-based test suite is now working with the OAuth2
Implicit Grant.
---
common/common.sh | 41 +++----
docker-run-tests.sh | 257 +++-----------------------------------------
local-run-tests.sh | 5 +-
tests/Dockerfile | 5 +-
4 files changed, 44 insertions(+), 264 deletions(-)
diff --git a/common/common.sh b/common/common.sh
index 38c3736..8e55e15 100755
--- a/common/common.sh
+++ b/common/common.sh
@@ -222,27 +222,36 @@ configureKeycloak()
}'
check_return_code $?
- declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/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}"
+ declare -g ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/realms/debugger-testing/.well-known/openid-configuration"
+ declare -g ${FLOW_VARIABLE}_CLIENT_ID="${CLIENT_CLIENTID}"
+ declare -g ${FLOW_VARIABLE}_CLIENT_SECRET="${CLIENT_SECRET}"
+ declare -g ${FLOW_VARIABLE}_SCOPE="${SCOPE_NAME}"
+ declare -g ${FLOW_VARIABLE}_USER="${USER_ID}"
+
+ VAR_NAME1=${FLOW_VARIABLE}_DISCOVERY_ENDPOINT
+ VAR_NAME2=${FLOW_VARIABLE}_CLIENT_ID
+ VAR_NAME3=${FLOW_VARIABLE}_CLIENT_SECRET
+ VAR_NAME4=${FLOW_VARIABLE}_SCOPE
+ VAR_NAME5=${FLOW_VARIABLE}_USER
+
+ echo "${VAR_NAME1}=${!VAR_NAME1}"
+ echo "${VAR_NAME2}=${!VAR_NAME2}"
+ echo "${VAR_NAME3}=${!VAR_NAME3}"
+ echo "${VAR_NAME4}=${!VAR_NAME4}"
+ echo "${VAR_NAME5}=${!VAR_NAME5}"
done
echo "Leaving configureKeycloak()."
- env
- exit 1
}
runTests()
{
echo "Entering runTests()."
- env
# Test client credentials flow
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 --url "${DEBUGGER_BASE_URL}"
+ node ${NODEJS_BASE_DIR}/oauth2_client_credentials.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
# Test authorization code flow
@@ -262,7 +271,7 @@ runTests()
SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE} \
USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER} \
PKCE_ENABLED=${PKCE_ENABLED} \
- node tests/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
+ node ${NODEJS_BASE_DIR}/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
@@ -279,7 +288,7 @@ runTests()
SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE} \
USER=${AUTHORIZATION_CODE_PUBLIC_USER} \
PKCE_ENABLED=${PKCE_ENABLED} \
- node tests/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
+ node ${NODEJS_BASE_DIR}/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
done
@@ -288,16 +297,8 @@ runTests()
CLIENT_ID=${IMPLICIT_CLIENT_ID} \
SCOPE=${IMPLICIT_SCOPE} \
USER=${IMPLICIT_USER} \
- node tests/oauth2_implicit.js --url "${DEBUGGER_BASE_URL}"
+ node ${NODEJS_BASE_DIR}/oauth2_implicit.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
echo "Leaving runTests()."
}
-
-execute()
-{
- configureKeycloak
- check_return_code $?
- runTests
- check_return_code $?
-}
diff --git a/docker-run-tests.sh b/docker-run-tests.sh
index 79ac4ff..6d9ba64 100755
--- a/docker-run-tests.sh
+++ b/docker-run-tests.sh
@@ -5,252 +5,27 @@ init()
{
DEBUGGER_BASE_URL=http://client:3000
CONFIG_FILE=./env/local.js
-}
-
-check_return_code()
-{
- rc=$1
- if [ $rc -ne 0 ];
+ KEYCLOAK_BASE_URL=http://keycloak:8080
+ KEYCLOAK_LOCALHOST_BASE_URL=http://keycloak:8080
+ CONFIG_FILE=./env/local.js
+ CURRENT_DIR=`echo "$(dirname "$(realpath "$0")")"`
+ COMMON_SH=${CURRENT_DIR}/common.sh
+ if [ -r "${COMMON_SH}" ];
then
- echo "Non-zero return code. Exiting."
+ . ${COMMON_SH}
+ else
+ echo "Cannot find ${COMMON_SH}."
exit 1
fi
+ NODEJS_BASE_DIR=.
}
init
-KEYCLOAK_ACCESS_TOKEN=$(curl \
- -X POST "http://keycloak: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')
-if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
-then
- echo "Failed to obtain access token."
- exit 1
-fi
-
-curl -X POST "http://keycloak:8080/admin/realms" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- -H "Content-Type: application/json" \
- -d '{"realm": "debugger-testing", "enabled": true}'
check_return_code $?
-
-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://keycloak: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')
- if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ];
- then
- echo "KEYCLOAK_ACCESS_TOKEN is blank."
- exit 1
- fi
- curl -X POST "http://keycloak: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"
- }
- }'
- check_return_code $?
- case "${FLOW_VARIABLE}" in
- CLIENT_CREDENTIALS)
- curl -X POST "http://keycloak: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"
- }'
- check_return_code $?
- ;;
- AUTHORIZATION_CODE_CONFIDENTIAL)
- curl -X POST "http://keycloak: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": ["'${DEBUGGER_BASE_URL}/callback'"],
- "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
- "attributes": {
- "frontchannel.logout.url":
- "'${DEBUGGER_BASE_URL}/logout'"
- }
- }'
- check_return_code $?
- ;;
- AUTHORIZATION_CODE_PUBLIC)
- curl -X POST "http://keycloak: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": ["'${DEBUGGER_BASE_URL}/callback'"],
- "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
- "attributes": {
- "frontchannel.logout.url":
- "'${DEBUGGER_BASE_URL}/logout'"
- }
- }'
- check_return_code $?
- ;;
- esac
-
- CLIENT_ID=$(curl \
- -X GET \
- "http://keycloak:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].id')
- CLIENT_CLIENTID=$(curl \
- -X GET \
- "http://keycloak:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].clientId')
- CLIENT_SECRET=$(curl \
- -X GET \
- "http://keycloak:8080/admin/realms/debugger-testing/clients?clientId=${FLOW_NAME}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[0].secret')
- SCOPE_ID=$(curl \
- -X GET \
- "http://keycloak:8080/admin/realms/debugger-testing/client-scopes" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \
- | jq -r '.[] | select(.name=="'${FLOW_NAME}'-scope") | .id')
- SCOPE_NAME=$(curl \
- -X GET \
- "http://keycloak: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://keycloak:8080/admin/realms/debugger-testing/clients/${CLIENT_ID}/optional-client-scopes/${SCOPE_ID}" \
- -H "Authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}"
- check_return_code $?
- USER_ID=$(curl \
- -X POST \
- "http://keycloak: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')
- if [ -z "${CLIENT_ID}" ] || \
- [ -z "${CLIENT_CLIENTID}" ] || \
- [ -z "${CLIENT_SECRET}" ] || \
- [ -z "${SCOPE_ID}" ] || \
- [ -z "${SCOPE_NAME} ] || \
- [ -z "${USER_ID} ];
- then
- echo "Required variable is blank."
- exit 1
- fi
- curl \
- -X PUT \
- "http://keycloak: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
- }'
- check_return_code $?
-
- declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="http://keycloak: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=${CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT} \
-CLIENT_ID=${CLIENT_CREDENTIALS_CLIENT_ID} \
-CLIENT_SECRET=${CLIENT_CREDENTIALS_CLIENT_SECRET} \
-SCOPE=${CLIENT_CREDENTIALS_SCOPE} \
-node oauth2_client_credentials.js --url "${DEBUGGER_BASE_URL}"
+configureKeycloak
check_return_code $?
-
-# Test authorization code flow
-for PKCE_ENABLED in true false
-do
- echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
- echo "CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
- echo "CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
- echo "SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}"
- echo "USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER}"
- echo "PKCE_ENABLED=${PKCE_ENABLED}"
-
- # 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 oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
- check_return_code $?
-
- echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
- echo "CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
- echo "CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
- echo "SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE}"
- echo "USER=${AUTHORIZATION_CODE_PUBLIC_USER}"
- echo "PKCE_ENABLED=${PKCE_ENABLED}"
-
- # 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 oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
- check_return_code $?
-done
+runTests
+check_return_code $?
+node --version
+check_return_code $?
+exit 0
diff --git a/local-run-tests.sh b/local-run-tests.sh
index d2e2f13..3c8f4ad 100755
--- a/local-run-tests.sh
+++ b/local-run-tests.sh
@@ -18,6 +18,7 @@ init()
echo "Cannot find ${COMMON_SH}."
exit 1
fi
+ NODEJS_BASE_DIR=tests
}
prepTestEnv()
@@ -40,7 +41,9 @@ startDocker
check_return_code $?
sleep 60
check_return_code $?
-execute
+configureKeycloak
+check_return_code $?
+runTests
check_return_code $?
node --version
check_return_code $?
diff --git a/tests/Dockerfile b/tests/Dockerfile
index 40e7319..5334db2 100644
--- a/tests/Dockerfile
+++ b/tests/Dockerfile
@@ -25,6 +25,7 @@ WORKDIR /usr/src/app
# where available (npm@5+)
COPY tests/package*.json ./
COPY docker-run-tests.sh .
+COPY common/common.sh .
# Install NVM
ENV NVM_DIR /usr/local/nvm
@@ -50,8 +51,8 @@ RUN node -v
RUN npm -v
# Bundle app source
-COPY tests/. .
-
+COPY tests/oauth2_* ./
+RUN ls
RUN wget -q -O chrome-linux64.zip https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/121.0.6167.85/linux64/chrome-linux64.zip && \
unzip chrome-linux64.zip && \
rm chrome-linux64.zip && \
From af5076a852729872336a5ae1144d0c75c8adb767 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:44:32 -0700
Subject: [PATCH 05/11] Added logout test to the OAuth2 Implicit Grant test.
---
tests/oauth2_implicit.js | 50 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 49 insertions(+), 1 deletion(-)
diff --git a/tests/oauth2_implicit.js b/tests/oauth2_implicit.js
index eeaf1cc..ac5e08f 100644
--- a/tests/oauth2_implicit.js
+++ b/tests/oauth2_implicit.js
@@ -5,7 +5,8 @@ const jwt = require("jsonwebtoken");
const assert = require("assert");
const { Command, Option } = require('commander');
-var baseUrl = "http://localhost:3000"
+var baseUrl = "http://localhost:3000";
+var logout_post_redirect_uri_value = baseUrl + "/logout.html";
var headless = true;
async function populateMetadata(driver, discovery_endpoint) {
@@ -153,6 +154,50 @@ async function verifyAccessToken(access_token, client_id, scope, user) {
assert.strictEqual(decoded_access_token.payload.email, `${client_id}@iyasec.io`, "Access token email does not match.");
}
+async function logout(driver) {
+ console.log("Entering logout().");
+ console.log("Find logout Button");
+ logout_button = By.id("logout_btn");
+ console.log("Find logout_post_redirect_uri.");
+ logout_post_redirect_uri = By.id("logout_post_redirect_uri");
+ console.log("Wait for logout_post_redirect_uri.");
+ await driver.wait(until.elementLocated(logout_post_redirect_uri), 10000);
+ console.log("Wait for logout_post_redirect_uri to be visible.");
+ await driver.findElement(logout_post_redirect_uri).clear();
+ await driver.wait(until.elementIsVisible(driver.findElement(logout_post_redirect_uri)), 10000);
+ console.log("Set post_redirect_uri for logout.");
+ await driver.findElement(logout_post_redirect_uri).sendKeys(logout_post_redirect_uri_value);
+ console.log("Click logout_btn.");
+ await driver.findElement(logout_button).click();
+
+ console.log("Wait for kc_logout.");
+ kc_logout = By.id("kc-logout");
+ await driver.wait(until.elementLocated(kc_logout), 10000);
+ console.log("Wait for kc-logout to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(kc_logout)), 10000);
+
+ console.log("Click kc_logout.");
+ await driver.findElement(kc_logout).click();
+
+ console.log("Click link to return to the front page of the debugger.");
+ returnToDebugLink = By.partialLinkText('Return to debugger');
+ await driver.wait(until.elementLocated(returnToDebugLink), 10000);
+ await driver.findElement(returnToDebugLink).click();
+
+ console.log("Find authz_expand_button.");
+ authz_expand_button = By.id("authz_expand_button");
+ await driver.wait(until.elementLocated(authz_expand_button), 10000);
+ console.log("Waiting for authz_expand_button to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000);
+
+ console.log("Find client_id.");
+ client_id = By.id("client_id");
+ console.log("Wait for client_id");
+ await driver.findElement(client_id);
+ console.log("Wait for client_id to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(client_id)), 10000);
+}
+
async function test() {
const options = new chrome.Options();
if(headless) {
@@ -188,6 +233,8 @@ async function test() {
console.log("Access token: " + access_token);
console.log("Calling verifyAccessToken().");
await verifyAccessToken(access_token, client_id, scope, user);
+ console.log("Logging out.");
+ await logout(driver);
console.log("Test completed successfully.")
} catch (error) {
console.log(error.message);
@@ -216,6 +263,7 @@ program
if(!!options.url) {
console.log("Setting url to " + options.url);
baseUrl = options.url;
+ logout_post_redirect_uri_value = options.url + "/logout.html";
}
if(!!options.browser) {
console.log("Using browser. headless = false.");
From cfdb0bea2680ee474a5acb5e85eab3489e5ab3d2 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Fri, 31 Oct 2025 23:53:43 -0700
Subject: [PATCH 06/11] Updating docker-compose file name for containerized
tests. Updated environment file path.
---
.github/workflows/tests.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e9adb54..50049e0 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -18,4 +18,4 @@ jobs:
with:
version: '2.14.2'
- name: Run Tests
- run: sudo CONFIG_FILE=./env/local.js docker compose -f docker-compose-with-keycloak.yml up --abort-on-container-exit
+ run: sudo CONFIG_FILE=./env/docker-tests.js docker compose -f docker-compose-run-tests.yml up --abort-on-container-exit
From 6fd61ecdc7101c23f845d0f81d2c140cd1ece74a Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Sat, 1 Nov 2025 00:05:16 -0700
Subject: [PATCH 07/11] Adding repo checkout back into tests pipeline.
---
.github/workflows/tests.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 50049e0..079d070 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,6 +13,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
- name: Install docker-compose
uses: KengoTODA/actions-setup-docker-compose@v1
with:
From 507e36821183ab622e92725044a17d6a5ea5b41a Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Sat, 1 Nov 2025 01:35:34 -0700
Subject: [PATCH 08/11] Added logout step to OAuth2 Authorization Code test.
---
tests/oauth2_authorization_code.js | 62 ++++++++++++++++++++++++------
1 file changed, 50 insertions(+), 12 deletions(-)
diff --git a/tests/oauth2_authorization_code.js b/tests/oauth2_authorization_code.js
index f7892fd..833482f 100644
--- a/tests/oauth2_authorization_code.js
+++ b/tests/oauth2_authorization_code.js
@@ -6,6 +6,7 @@ const assert = require("assert");
const { Command, Option } = require('commander');
var baseUrl = "http://localhost:3000"
+var logout_post_redirect_uri_value = baseUrl + "/logout.html";
var headless = true;
async function populateMetadata(driver, discovery_endpoint) {
@@ -203,6 +204,50 @@ async function verifyAccessToken(access_token, client_id, scope, user) {
assert.strictEqual(decoded_access_token.payload.email, `${client_id}@iyasec.io`, "Access token email does not match.");
}
+async function logout(driver) {
+ console.log("Entering logout().");
+ console.log("Find logout Button");
+ logout_button = By.id("logout_btn");
+ console.log("Find logout_post_redirect_uri.");
+ logout_post_redirect_uri = By.id("logout_post_redirect_uri");
+ console.log("Wait for logout_post_redirect_uri.");
+ await driver.wait(until.elementLocated(logout_post_redirect_uri), 10000);
+ console.log("Wait for logout_post_redirect_uri to be visible.");
+ await driver.findElement(logout_post_redirect_uri).clear();
+ await driver.wait(until.elementIsVisible(driver.findElement(logout_post_redirect_uri)), 10000);
+ console.log("Set post_redirect_uri for logout.");
+ await driver.findElement(logout_post_redirect_uri).sendKeys(logout_post_redirect_uri_value);
+ console.log("Click logout_btn.");
+ await driver.findElement(logout_button).click();
+
+ console.log("Wait for kc_logout.");
+ kc_logout = By.id("kc-logout");
+ await driver.wait(until.elementLocated(kc_logout), 10000);
+ console.log("Wait for kc-logout to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(kc_logout)), 10000);
+
+ console.log("Click kc_logout.");
+ await driver.findElement(kc_logout).click();
+
+ console.log("Click link to return to the front page of the debugger.");
+ returnToDebugLink = By.partialLinkText('Return to debugger');
+ await driver.wait(until.elementLocated(returnToDebugLink), 10000);
+ await driver.findElement(returnToDebugLink).click();
+
+ console.log("Find authz_expand_button.");
+ authz_expand_button = By.id("authz_expand_button");
+ await driver.wait(until.elementLocated(authz_expand_button), 10000);
+ console.log("Waiting for authz_expand_button to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000);
+
+ console.log("Find client_id.");
+ client_id = By.id("client_id");
+ console.log("Wait for client_id");
+ await driver.findElement(client_id);
+ console.log("Wait for client_id to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(client_id)), 10000);
+}
+
async function test() {
const options = new chrome.Options();
if(headless) {
@@ -210,11 +255,8 @@ async function test() {
}
options.addArguments("--no-sandbox");
console.log("Enabling selinium logging.");
-// var logger = logging.getLogger('webdriver')
-// logger.setLevel(logging.Level.FINEST)
-// logging.installConsoleHandler()
- const loggingPrefs = new logging.Preferences();
- loggingPrefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
+ const loggingPrefs = new logging.Preferences();
+ loggingPrefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
const driver = await new Builder()
.forBrowser("chrome")
@@ -246,13 +288,6 @@ async function test() {
process.exit(1);
}
- console.log("Enabling selinium logging.");
-// var logger = logging.getLogger('webdriver')
-// logger.setLevel(logging.Level.FINEST)
-// logging.installConsoleHandler()
- const loggingPrefs = new logging.Preferences();
- loggingPrefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
-
console.log("Kicking off test.");
await driver.get(baseUrl);
console.log("Calling populateMetadata().");
@@ -262,6 +297,8 @@ async function test() {
console.log("Access token: " + access_token);
console.log("Calling verifyAccessToken().");
await verifyAccessToken(access_token, client_id, scope, user);
+ console.log("Logging out.");
+ await logout(driver);
console.log("Test completed successfully.")
} catch (error) {
console.log(error.message);
@@ -290,6 +327,7 @@ program
if(!!options.url) {
console.log("Setting url to " + options.url);
baseUrl = options.url;
+ logout_post_redirect_uri_value = options.url + "/logout.html";
}
if(!!options.browser) {
console.log("Using browser. headless = false.");
From f3a751f1856b60ffc2f4d833d93ebf52da80a565 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Mon, 3 Nov 2025 08:48:17 -0800
Subject: [PATCH 09/11] OIDC Authorization Code Flow (Public Client) test
working with token detail page tests for all token types.
---
api/server.js | 50 +++-
client/public/token_detail.html | 4 +-
client/src/debugger2.js | 10 +-
common/common.sh | 100 ++++++-
tests/oidc_authorization_code.js | 497 +++++++++++++++++++++++++++++++
tests/package-lock.json | 74 +++--
tests/package.json | 2 +-
7 files changed, 695 insertions(+), 42 deletions(-)
create mode 100644 tests/oidc_authorization_code.js
diff --git a/api/server.js b/api/server.js
index 8407793..4d69db4 100644
--- a/api/server.js
+++ b/api/server.js
@@ -57,16 +57,43 @@ app.get('/healthcheck', function (req, res) {
* @returns {Error.model} 500 - Unexpected error
*/
app.get('/claimdescription', function(req, res) {
- fetch("https://www.iana.org/assignments/jwt/jwt.xml")
- .then((response) => {
- response.text()
- .then( (text) => {
- log.debug("Retrieved: " + text);
- res
- .append('Content-Type', 'application/xml')
- .send(text)
+ try {
+ fetch("https://www.iana.org/assignments/jwt/jwt.xml")
+ .then((response) => {
+ response.text()
+ .then( (text) => {
+ log.debug("Retrieved: " + text);
+ res
+ .append('Content-Type', 'application/xml')
+ .send(text)
+ });
+ })
+ .catch(function (error) {
+ log.error('Error from claimsdescription endpoint: ' + error);
+ if(!!error.response) {
+ if(!!error.response.status) {
+ log.error("Error Status: " + error.response.status);
+ }
+ if(!!error.response.data) {
+ log.error("Error Response body: " + JSON.stringify(error.response.data));
+ }
+ if(!!error.response.headers) {
+ log.error("Error Response headers: " + error.response.headers);
+ }
+ if (!!error.response) {
+ res.status(error.response.status);
+ res.json(error.response.data);
+ } else {
+ res.status(500);
+ res.json(error.message);
+ }
+ }
});
- });
+ } catch(e) {
+ log.error("An error occurred while retrieving the claim description XML: " + e.stack);
+ res.status(500)
+ .render('error', { error: e });
+ }
});
/**
@@ -203,6 +230,7 @@ app.post('/token', (req, res) => {
* @returns {Error.model} 500 - Unexpected error
*/
app.post('/introspection', (req, res) => {
+try {
log.info('Entering app.post for /introspection.');
const body = req.body;
log.debug('body: ' + JSON.stringify(body));
@@ -253,7 +281,9 @@ app.post('/introspection', (req, res) => {
}
}
});
-
+ } catch(e) {
+ log.error("Error from OAuth2 Introspection Endpoint: " + error);
+ }
});
let options = {
diff --git a/client/public/token_detail.html b/client/public/token_detail.html
index d887a8a..f63ec98 100644
--- a/client/public/token_detail.html
+++ b/client/public/token_detail.html
@@ -44,8 +44,8 @@
Decoded Token
- JSON
- Key Pairs
+ JSON
+ Key Pairs
diff --git a/client/src/debugger2.js b/client/src/debugger2.js
index 2d225f3..f038624 100644
--- a/client/src/debugger2.js
+++ b/client/src/debugger2.js
@@ -308,7 +308,7 @@ function successfulInternalTokenAPICall(data, textStatus, request)
log.debug("Refresh token found. Generating token: data.refresh_token=" + currentRefreshToken);
token_endpoint_result_html += "" +
'' +
- 'Refresh Token ' +
+ 'Refresh Token ' +
'
' +
' ' +
@@ -1163,6 +1163,8 @@ function onload() {
if (!appconfig) {
log.debug('Failed to load appconfig.');
}
+
+ var authorization_grant_type = $("#authorization_grant_type").val();
$("#password-form-group1").hide();
$("#password-form-group2").hide();
@@ -1265,8 +1267,8 @@ function onload() {
displayTokenCustomParametersCheck();
if( getParameterByName("redirectFromTokenDetail") == "true" &&
- ( value != "implicit_grant" &&
- value != "oidc_implicit_grant"))
+ ( authorization_grant_type != "implicit_grant" &&
+ authorization_grant_type != "oidc_implicit_grant"))
{
log.debug('Detected redirect back from token detail page.');
$("#step3").hide();
@@ -1684,7 +1686,7 @@ function recreateTokenDisplay()
log.debug("Displaying refresh token");
token_endpoint_result_html += " " +
'' +
- 'Refresh Token ' +
+ 'Refresh Token ' +
'
' +
' ' +
diff --git a/common/common.sh b/common/common.sh
index 8e55e15..c2c370d 100755
--- a/common/common.sh
+++ b/common/common.sh
@@ -35,7 +35,7 @@ configureKeycloak()
-d '{"realm": "debugger-testing", "enabled": true}'
check_return_code $?
- for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC IMPLICIT
+ for FLOW_VARIABLE in CLIENT_CREDENTIALS AUTHORIZATION_CODE_CONFIDENTIAL AUTHORIZATION_CODE_PUBLIC IMPLICIT OIDC_AUTHORIZATION_CODE_CONFIDENTIAL OIDC_AUTHORIZATION_CODE_PUBLIC
do
FLOW_NAME=$(echo ${FLOW_VARIABLE} | tr '[:upper:]' '[:lower:]' | tr '_' '-')
@@ -150,6 +150,53 @@ configureKeycloak()
}'
check_return_code $?
;;
+ OIDC_AUTHORIZATION_CODE_PUBLIC)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ check_return_code $?
+ ;;
+ OIDC_AUTHORIZATION_CODE_CONFIDENTIAL)
+ curl -X POST "${KEYCLOAK_LOCALHOST_BASE_URL}/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": ["'${DEBUGGER_BASE_URL}/callback'"],
+ "webOrigins": ["/*", "'${DEBUGGER_BASE_URL}/*'"],
+ "attributes": {
+ "frontchannel.logout.url": "'${DEBUGGER_BASE_URL}/logout'",
+ "post.logout.redirect.uris": "'${DEBUGGER_BASE_URL}/logout.html'"
+ }
+ }'
+ check_return_code $?
+ ;;
+
esac
CLIENT_ID=$(curl \
@@ -222,6 +269,7 @@ configureKeycloak()
}'
check_return_code $?
+ declare -g ${FLOW_VARIABLE}_AUDIENCE="${KEYCLOAK_BASE_URL}/realms/debugger-testing"
declare -g ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="${KEYCLOAK_BASE_URL}/realms/debugger-testing/.well-known/openid-configuration"
declare -g ${FLOW_VARIABLE}_CLIENT_ID="${CLIENT_CLIENTID}"
declare -g ${FLOW_VARIABLE}_CLIENT_SECRET="${CLIENT_SECRET}"
@@ -233,12 +281,14 @@ configureKeycloak()
VAR_NAME3=${FLOW_VARIABLE}_CLIENT_SECRET
VAR_NAME4=${FLOW_VARIABLE}_SCOPE
VAR_NAME5=${FLOW_VARIABLE}_USER
+ VAR_NAME6=${FLOW_VARIABLE}_AUDIENCE
echo "${VAR_NAME1}=${!VAR_NAME1}"
echo "${VAR_NAME2}=${!VAR_NAME2}"
echo "${VAR_NAME3}=${!VAR_NAME3}"
echo "${VAR_NAME4}=${!VAR_NAME4}"
echo "${VAR_NAME5}=${!VAR_NAME5}"
+ echo "${VAR_NAME6}=${!VAR_NAME6}"
done
echo "Leaving configureKeycloak()."
}
@@ -247,6 +297,7 @@ runTests()
{
echo "Entering runTests()."
# Test client credentials flow
+ AUDIENCE=${CLIENT_CREDENTIALS_AUDIENCE} \
DISCOVERY_ENDPOINT=${CLIENT_CREDENTIALS_DISCOVERY_ENDPOINT} \
CLIENT_ID=${CLIENT_CREDENTIALS_CLIENT_ID} \
CLIENT_SECRET=${CLIENT_CREDENTIALS_CLIENT_SECRET} \
@@ -257,6 +308,7 @@ runTests()
# Test authorization code flow
for PKCE_ENABLED in true false
do
+ echo "AUDIENCE=${AUTHORIZATION_CODE_CONFIDENTIAL_AUDIENCE}"
echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
echo "CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
echo "CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
@@ -265,6 +317,7 @@ runTests()
echo "PKCE_ENABLED=${PKCE_ENABLED}"
# Confidential client
+ AUDIENCE=${AUTHORIZATION_CODE_CONFIDENTIAL_AUDIENCE} \
DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT} \
CLIENT_ID=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID} \
CLIENT_SECRET=${AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET} \
@@ -274,6 +327,7 @@ runTests()
node ${NODEJS_BASE_DIR}/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
+ echo "AUDIENCE=${AUTHORIZATION_CODE_PUBLIC_AUDIENCE}"
echo "DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
echo "CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
echo "CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
@@ -282,6 +336,7 @@ runTests()
echo "PKCE_ENABLED=${PKCE_ENABLED}"
# Public client
+ AUDIENCE=${AUTHORIZATION_CODE_PUBLIC_AUDIENCE} \
DISCOVERY_ENDPOINT=${AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT} \
CLIENT_ID=${AUTHORIZATION_CODE_PUBLIC_CLIENT_ID} \
CLIENT_SECRET=${AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET} \
@@ -293,6 +348,7 @@ runTests()
done
# OAuth2 Implicit Grant
+ AUDIENCE=${IMPLICIT_AUDIENCE} \
DISCOVERY_ENDPOINT=${IMPLICIT_DISCOVERY_ENDPOINT} \
CLIENT_ID=${IMPLICIT_CLIENT_ID} \
SCOPE=${IMPLICIT_SCOPE} \
@@ -300,5 +356,47 @@ runTests()
node ${NODEJS_BASE_DIR}/oauth2_implicit.js --url "${DEBUGGER_BASE_URL}"
check_return_code $?
+ # Test OIDC Authorization Code Flow
+ for PKCE_ENABLED in true false
+ do
+ echo "AUDIENCE=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_AUDIENCE}"
+ echo "DISCOVERY_ENDPOINT=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID}"
+ echo "CLIENT_SECRET=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET}"
+ echo "SCOPE=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}"
+ echo "USER=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
+ # Confidential client
+ AUDIENCE=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_AUDIENCE} \
+ DISCOVERY_ENDPOINT=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_DISCOVERY_ENDPOINT} \
+ CLIENT_ID=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_ID} \
+ CLIENT_SECRET=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_CLIENT_SECRET} \
+ SCOPE="openid profile email offline_access ${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}" \
+ USER=${OIDC_AUTHORIZATION_CODE_CONFIDENTIAL_USER} \
+ PKCE_ENABLED=${PKCE_ENABLED} \
+ node ${NODEJS_BASE_DIR}/oidc_authorization_code.js --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+
+ echo "AUDIENCE=${OIDC_AUTHORIZATION_CODE_PUBLIC_AUDIENCE}"
+ echo "DISCOVERY_ENDPOINT=${OIDC_AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT}"
+ echo "CLIENT_ID=${OIDC_AUTHORIZATION_CODE_PUBLIC_CLIENT_ID}"
+ echo "CLIENT_SECRET=${OIDC_AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET}"
+ echo "SCOPE={OIDC_AUTHORIZATION_CODE_PUBLIC_SCOPE}"
+ echo "USER=${OIDC_AUTHORIZATION_CODE_PUBLIC_USER}"
+ echo "PKCE_ENABLED=${PKCE_ENABLED}"
+
+ # Public client
+ AUDIENCE=${OIDC_AUTHORIZATION_CODE_PUBLIC_AUDIENCE} \
+ DISCOVERY_ENDPOINT=${OIDC_AUTHORIZATION_CODE_PUBLIC_DISCOVERY_ENDPOINT} \
+ CLIENT_ID=${OIDC_AUTHORIZATION_CODE_PUBLIC_CLIENT_ID} \
+ CLIENT_SECRET=${OIDC_AUTHORIZATION_CODE_PUBLIC_CLIENT_SECRET} \
+ SCOPE="openid profile email offline_access ${OIDC_AUTHORIZATION_CODE_PUBLIC_SCOPE}" \
+ USER=${OIDC_AUTHORIZATION_CODE_PUBLIC_USER} \
+ PKCE_ENABLED=${PKCE_ENABLED} \
+ node ${NODEJS_BASE_DIR}/oidc_authorization_code.js --url "${DEBUGGER_BASE_URL}"
+ check_return_code $?
+ done
+
echo "Leaving runTests()."
}
diff --git a/tests/oidc_authorization_code.js b/tests/oidc_authorization_code.js
new file mode 100644
index 0000000..c6f51b0
--- /dev/null
+++ b/tests/oidc_authorization_code.js
@@ -0,0 +1,497 @@
+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");
+const { Command, Option } = require('commander');
+
+var baseUrl = "http://localhost:3000"
+var logout_post_redirect_uri_value = baseUrl + "/logout.html";
+var headless = true;
+var audience = "http://localhost:8080/realms/debugger-testing";
+
+function decodeJWT(jwt_) {
+ return jwt.decode(jwt_, {complete: true});
+}
+
+const wait = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds));
+
+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) {
+ console.log("Entering getAccessToken().");
+ console.log("Find authorization_grant_type.");
+ authorization_grant_type = By.id("authorization_grant_type");
+ console.log("Find usePKCE-yes.");
+ usePKCE_yes = By.id("usePKCE-yes");
+ console.log("Find usePKCE-no.");
+ usePKCE_no = By.id("usePKCE-no");
+ console.log("Find authz_expand_button");
+ authz_expand_button = By.id("authz_expand_button");
+ console.log("Find client_id.");
+ client_id_ = By.id("client_id");
+ console.log("Find scope.");
+ scope_ = By.id("scope");
+ console.log("find token_client_id.");
+ token_client_id = By.id("token_client_id");
+ console.log("Find token_client_secret.");
+ token_client_secret = By.id("token_client_secret");
+ console.log("Find token_scope.");
+ token_scope = By.id("token_scope");
+ console.log("Find btn_authorize.");
+ btn_authorize = By.css("input[type=\"submit\"][value=\"Authorize\"]");
+ console.log("Find username.");
+ keycloak_username = By.id("username");
+ console.log("Find password.");
+ keycloak_password = By.id("password");
+ console.log("Find kc-login");
+ keycloak_kc_login = By.id("kc-login");
+ console.log("Find token_btn.");
+ token_btn = By.className("token_btn");
+ console.log("Find token_access_token.");
+ token_access_token = By.id("token_access_token");
+ console.log("Find display_token_error_form_texarea1.");
+ display_token_error_form_textarea1 = By.id("display_token_error_form_textarea1");
+
+ // Select client credential login type
+ console.log("Set authorization_grant_type to OIDC Authorizaton Code Authentication Flow.");
+ await new Select(await driver.findElement(authorization_grant_type)).selectByVisibleText('OIDC Authorization Code Flow(code)');
+ console.log("Waiting for usePKCE_yes");
+ await driver.wait(until.elementLocated(usePKCE_yes), 10000);
+ console.log("Waiting for usePKCE_yes to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(usePKCE_yes)), 10000);
+ console.log("Waiting for usePKCE_no.");
+ await driver.wait(until.elementLocated(usePKCE_no), 10000);
+ console.log("Waiting for usePKCE to be visible");
+ await driver.wait(until.elementIsVisible(driver.findElement(usePKCE_no)), 10000);
+
+ if (pkce_enabled) {
+ console.log("Click usePKCE_yes.");
+ await driver.findElement(usePKCE_yes).click();
+ } else {
+ console.log("Click usePKCE_no.");
+ await driver.findElement(usePKCE_no).click();
+ }
+
+ console.log("Find authz_expand_button.");
+ await driver.wait(until.elementLocated(authz_expand_button), 10000);
+ console.log("Waiting for authz_expand_button to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000);
+ console.log("Click authz_expand_button.");
+ await driver.findElement(authz_expand_button).click();
+ console.log("Locate client_id_.");
+ await driver.wait(until.elementLocated(client_id_), 10000);
+ console.log("Find client_id_.");
+ await driver.wait(until.elementIsVisible(driver.findElement(client_id_)), 10000);
+
+ // Submit credentials
+ console.log("Clear client_id_.");
+ await driver.findElement(client_id_).clear();
+ console.log("Set client_id value.");
+ await driver.findElement(client_id_).sendKeys(client_id);
+ console.log("Clear scope_.");
+ await driver.findElement(scope_).clear();
+ console.log("Set scope value.");
+ await driver.findElement(scope_).sendKeys(scope);
+ console.log("Find token_redirect_uri.");
+ redirect_uri = By.id("redirect_uri");
+ console.log("Clear redirect_uri.");
+ await driver.findElement(redirect_uri).clear();
+ console.log("Set redirect_uri value: redirect_uri=" + redirect_uri + ", redirect_uri=" + baseUrl + "/callback");
+ await driver.findElement(redirect_uri).sendKeys(baseUrl + "/callback");
+ console.log("Click btn_authorize button.");
+ await driver.findElement(btn_authorize).click();
+
+ // Login to Keycloak
+ try {
+ console.log("Wait for keycloak_username.");
+ await driver.wait(until.elementLocated(keycloak_username), 10000);
+ console.log("Wait for keycloak_username to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(keycloak_username)), 10000);
+ } catch (error) {
+ console.log("Unable to log into keycloak.");
+ 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());
+ }
+
+ console.log("Clear keycloak_username.");
+ await driver.findElement(keycloak_username).clear();
+ console.log("Set keycloak_username value.");
+ await driver.findElement(keycloak_username).sendKeys(client_id);
+ console.log("Clear keycloak_password.");
+ await driver.findElement(keycloak_password).clear();
+ console.log("Set client_id value.");
+ await driver.findElement(keycloak_password).sendKeys(client_id);
+ console.log("Click keycloak_kc_login button.");
+ await driver.findElement(keycloak_kc_login).click();
+
+ // Submit credentials (again)
+ console.log("Locate token_client_id.");
+ await driver.wait(until.elementLocated(token_client_id), 10000);
+ console.log("Wait for token_client_id to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(token_client_id)), 10000);
+
+ console.log("Clear token_client_id.");
+ await driver.findElement(token_client_id).clear();
+ console.log("Set token_client_id value.");
+ await driver.findElement(token_client_id).sendKeys(client_id);
+ console.log("Clear token_client_secret.");
+ await driver.findElement(token_client_secret).clear();
+ console.log("Set token_client_secret value.");
+ await driver.findElement(token_client_secret).sendKeys(client_secret);
+ console.log("Clear token_scope.");
+ await driver.findElement(token_scope).clear();
+ console.log("Set token_scope value.");
+ await driver.findElement(token_scope).sendKeys(scope);
+ console.log("Find token_redirect_uri.");
+ token_redirect_uri = By.id("token_redirect_uri");
+ console.log("Clear token_redirect_uri.");
+ await driver.findElement(token_redirect_uri).clear();
+ console.log("Set token_redirect_uri value: token_redirect_uri=" + token_redirect_uri + ", redirect_uri=" + baseUrl + "/callback");
+ await driver.findElement(token_redirect_uri).sendKeys(baseUrl + "/callback");
+ console.log("Click token_btn button.");
+ await driver.findElement(token_btn).click();
+
+ // Get access token result
+ async function waitForVisibility(element) {
+ console.log("Waiting for " + element);
+ await driver.wait(until.elementLocated(element), 10000);
+ console.log("Waiting for " + element + "is visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(element)), 10000);
+ console.log("Returning " + element);
+ return element;
+ }
+
+ let visibleAccessTokenElement = await Promise.any([
+ waitForVisibility(token_access_token),
+ waitForVisibility(display_token_error_form_textarea1)
+ ]);
+
+ console.log("Begin returning token.");
+ return await driver.findElement(visibleAccessTokenElement).getAttribute("value");
+}
+
+async function verifyAccessToken(access_token, client_id, scope, user, audience, issuer) {
+ 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: access_token.payload.sub=" + decoded_access_token.payload.sub + " , user=" + user);
+ assert.strictEqual(decoded_access_token.payload.aud, audience, "Access token aud does not match " + audience);
+ assert.strictEqual(decoded_access_token.payload.iss, issuer, "Access token iss does not match " + issuer);
+ 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.");
+ assert.strictEqual(decoded_access_token.payload.typ, "Bearer", "Access Token typ does not match.");
+}
+
+async function getIDToken(driver)
+{
+ console.log("Entering getIDToken().");
+ console.log("Find token_id_token.");
+ token_id_token = By.id("token_id_token");
+ console.log("Find token_id_token element.");
+ return await driver.findElement(token_id_token).getAttribute("value");
+}
+
+async function getRefreshToken(driver)
+{
+ console.log("Entering getRefreshToken().");
+ console.log("Find token_refresh_token.");
+ let token_refresh_token = By.id("token_refresh_token");
+ console.log("Find token_refresh_token element.");
+ return await driver.findElement(token_refresh_token).getAttribute("value");
+}
+
+
+async function verifyIDToken(id_token, client_id, user, audience, issuer) {
+ console.log("Entering verifyIDToken().");
+ let decoded_id_token = jwt.decode(id_token, { complete: true });
+ let response_text = id_token.match(/responseText: (.*)/);
+
+ assert.notStrictEqual(decoded_id_token, null, "Cannot decode ID token. Request result: " + (response_text ? response_text[1] : "no response text"));
+ assert.strictEqual(decoded_id_token.payload.azp, client_id, "ID token AZP does not match client ID.");
+ assert.strictEqual(decoded_id_token.payload.aud, audience, "ID token aud does not match " + audience);
+ assert.strictEqual(decoded_id_token.payload.iss, issuer, "ID token iss does not match " + issuer);
+ assert.strictEqual(decoded_id_token.payload.sub, user, "ID token SUB does not match user ID.");
+ assert.strictEqual(decoded_id_token.payload.given_name, client_id, "ID token given_name does not match.");
+ assert.strictEqual(decoded_id_token.payload.family_name, client_id, "ID token family_name does not match.");
+ assert.strictEqual(decoded_id_token.payload.email, `${client_id}@iyasec.io`, "ID token email does not match.");
+ assert.strictEqual(decoded_id_token.payload.typ, "ID", "ID Token typ does not match.");
+}
+
+async function verifyRefreshToken(refresh_token, client_id, user, audience, issuer) {
+ console.log("Entering verifyRefreshToken().");
+ let decoded_refresh_token = jwt.decode(refresh_token, { complete: true });
+ let response_text = refresh_token.match(/responseText: (.*)/);
+
+ assert.notStrictEqual(decoded_refresh_token, null, "Cannot decode Refresh token. Request result: " + (response_text ? response_text[1] : "no response text"));
+ assert.strictEqual(decoded_refresh_token.payload.aud, audience, "Refresh token aud does not match " + audience);
+ assert.strictEqual(decoded_refresh_token.payload.iss, issuer, "Refresh token iss does not match " + issuer);
+ assert.strictEqual(decoded_refresh_token.payload.azp, client_id, "Refresh token AZP does not match client ID.");
+ assert.strictEqual(decoded_refresh_token.payload.typ, "Offline", "Refresh Token typ does not match.");
+}
+
+async function tokenDetailPage(driver, type)
+{
+ console.log("Entering tokenDetailPage(). type=" + type + ".");
+ try {
+ var token_field = "";
+ var link_text = "";
+ if ( type === "access_token") {
+ token_field = "token_access_token";
+ link_text = "Access Token";
+ } else if ( type === "refresh_token") {
+ token_field = "token_refresh_token";
+ link_text = "Refresh Token";
+ } else if ( type === "id_token") {
+ token_field = "token_id_token";
+ link_text = "ID Token";
+ }
+ // Find the token detail link on the debugger2.html page.
+ console.log("Find token detail link.");
+ tokenDetailLink = By.partialLinkText(link_text);
+ console.log("Locate token detail link.");
+ await driver.wait(until.elementLocated(tokenDetailLink), 10000);
+ console.log("Click link to go to the token detail page for" + type + " token.");
+ await driver.findElement(tokenDetailLink).click();
+
+ // Find the jwt_payload field to confirm you are on the token_detail.html page.
+ var jwt_payload = By.id("jwt_payload");
+ console.log("Waiting for jwt_payload");
+ var jwt_payload_element = await driver.wait(until.elementLocated(jwt_payload), 10000);
+ console.log("jwt_payload_element: " + JSON.stringify(jwt_payload_element));
+ console.log("Waiting for jwt_payload to be visible.");
+ await driver.wait(until.elementIsVisible(jwt_payload_element), 10000);
+
+ // Confirm that the value in the jwt_payload text field matches the expected payload value.
+ token = await driver.executeScript("return window.localStorage.getItem(\"" + token_field + "\");")
+ console.log("token (from local storage): " + token);
+ console.log("Decode JWT.");
+ const decodedJWT = decodeJWT(token);
+ console.log("decodedJWT: " + JSON.stringify(decodedJWT.payload, null, 2));
+ console.log("Waiting ten seconds.");
+ await wait(10000);
+ console.log("Wait for JWT Payload to be populated in jwt_payload field.");
+ console.log("jwt_payload_element: " + JSON.stringify(jwt_payload_element));
+ const fromJWTPayloadJWT= await jwt_payload_element.getAttribute("value");
+ console.log("jwt_payload_element.text(): " + fromJWTPayloadJWT);
+ // await driver.wait(until.elementTextIs( jwt_payload_element, JSON.stringify(decodedJWT.payload, null, 2)), 10000);
+ if (fromJWTPayloadJWT === JSON.stringify(decodedJWT.payload, null, 2)) {
+ console.log("jwt_payload_element has expected value.");
+ } else {
+ console.log("jwt_payload_element does not have expected value.");
+ throw new Error("jwt_payload_element does not have expected value. jwt_payload.text=" +
+ fromJWTPayloadJWT +
+ ", localStorage('" +
+ token_field +
+ "')=" +
+ JSON.stringify(decodedJWT.payload, null, 2));
+ }
+
+ // Switch to the Key Pairs view.
+ console.log("Switch to the key pair view.");
+ keyPairButton = By.id("key_pair_button");
+ console.log("Locate key_pair_button.");
+ await driver.wait(until.elementLocated(keyPairButton), 10000);
+ console.log("Click button to switch to Key Pair View");
+ await driver.findElement(keyPairButton).click();
+
+ // Confirm key-pair view is visible.
+ keyPairJWTPayload = By.id("key_pair_jwt_payload");
+ console.log("Locate keyPairJWTPayload.");
+ await driver.wait(until.elementLocated(keyPairJWTPayload), 10000);
+ console.log("Wait for keyPairJWTPayload to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(keyPairJWTPayload)), 10000);
+
+ // Return to the debugger.
+ console.log("Find return to debugger link.");
+ returnToDebugger = By.partialLinkText('Return to debugger');
+ console.log("Locate return to debugger link.");
+ await driver.wait(until.elementLocated(returnToDebugger), 10000);
+ console.log("Click link to go back to debugger2.");
+ await driver.findElement(returnToDebugger).click();
+
+ // Make sure you see the access_token on the debugger2.html page.
+ console.log("Find token_access_token.");
+ token = By.id(token_field);
+ console.log("Wait for " + token_field);
+ await driver.findElement(token);
+ console.log("Wait for " + token_field + " to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(token)), 10000);
+ console.log("Leaving tokenDetailPage().");
+ } catch(e) {
+ console.log("An error occurred: " + e.stack);
+ process.exit(1);
+ }
+}
+
+async function logout(driver) {
+ console.log("Entering logout().");
+ console.log("Find logout Button");
+ logout_button = By.id("logout_btn");
+ console.log("Find logout_post_redirect_uri.");
+ logout_post_redirect_uri = By.id("logout_post_redirect_uri");
+ console.log("Wait for logout_post_redirect_uri.");
+ await driver.wait(until.elementLocated(logout_post_redirect_uri), 10000);
+ console.log("Wait for logout_post_redirect_uri to be visible.");
+ await driver.findElement(logout_post_redirect_uri).clear();
+ await driver.wait(until.elementIsVisible(driver.findElement(logout_post_redirect_uri)), 10000);
+ console.log("Set post_redirect_uri for logout.");
+ await driver.findElement(logout_post_redirect_uri).sendKeys(logout_post_redirect_uri_value);
+ console.log("Click logout_btn.");
+ await driver.findElement(logout_button).click();
+
+ returnToDebugLink = By.partialLinkText('Return to debugger');
+ console.log("Locate returnToDebugLink.");
+ await driver.wait(until.elementLocated(returnToDebugLink), 10000);
+ console.log("Click link to return to the front page of the debugger.");
+ await driver.findElement(returnToDebugLink).click();
+
+ console.log("Find authz_expand_button.");
+ authz_expand_button = By.id("authz_expand_button");
+ await driver.wait(until.elementLocated(authz_expand_button), 10000);
+ console.log("Waiting for authz_expand_button to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(authz_expand_button)), 10000);
+
+ console.log("Find client_id.");
+ client_id = By.id("client_id");
+ console.log("Wait for client_id");
+ await driver.findElement(client_id);
+ console.log("Wait for client_id to be visible.");
+ await driver.wait(until.elementIsVisible(driver.findElement(client_id)), 10000);
+}
+
+async function test() {
+ const options = new chrome.Options();
+ if(headless) {
+ 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;
+ const audience = process.env.AUDIENCE;
+ 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.");
+ assert(audience, "AUDIENCE 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);
+ }
+
+ console.log("Clear all cookies.");
+ await driver.manage().deleteAllCookies();
+ console.log("Kicking off test.");
+ await driver.get(baseUrl);
+ console.log("Calling populateMetadata().");
+ await populateMetadata(driver, discovery_endpoint);
+ console.log("Calling getAccessToken().");
+ let access_token = await getAccessToken(driver, client_id, client_secret, scope, pkce_enabled);
+ console.log("Access token: " + access_token);
+ console.log("Calling verifyAccessToken().");
+ await verifyAccessToken(access_token, client_id, scope, user, "account", audience);
+ console.log("Calling getIDToken().");
+ let id_token = await getIDToken(driver);
+ console.log("ID Token: " + id_token);
+ console.log("Calling verifyIDToken()");
+ await verifyIDToken(id_token, client_id, user, client_id, audience)
+ let refresh_token = await getRefreshToken(driver);
+ console.log("Refresh Token: " + refresh_token);
+ console.log("Calling verifyRefreshToken()");
+ await verifyRefreshToken(refresh_token, client_id, user, audience, audience)
+ console.log("Go to access_token detail page.");
+ await tokenDetailPage(driver, "access_token");
+ console.log("Go to refresh_token detail page.");
+ await tokenDetailPage(driver, "refresh_token");
+ console.log("Go to id_token detail page.");
+ await tokenDetailPage(driver, "id_token");
+ console.log("Logging out.");
+ await logout(driver);
+ console.log("Test completed successfully.")
+ } catch (error) {
+ console.log(error.message);
+ process.exit(1);
+ } finally {
+ await driver.quit();
+ }
+}
+
+const program = new Command();
+program
+ .name('oauth_authorization_code')
+ .description("Run test.")
+ .addOption(
+ new Option(
+ "-u, --url ",
+ "Set base URL.")
+ .makeOptionMandatory()
+ )
+ .addOption(
+ new Option(
+ "-b, --browser",
+ "Display browser (only works within device).")
+ )
+ .action((options) => {
+ if(!!options.url) {
+ console.log("Setting url to " + options.url);
+ baseUrl = options.url;
+ logout_post_redirect_uri_value = options.url + "/logout.html";
+ }
+ if(!!options.browser) {
+ console.log("Using browser. headless = false.");
+ headless = false;
+ }
+ });
+
+program.parse(process.argv).opts();
+
+test();
diff --git a/tests/package-lock.json b/tests/package-lock.json
index 507ec99..a2e6668 100644
--- a/tests/package-lock.json
+++ b/tests/package-lock.json
@@ -7,13 +7,13 @@
"dependencies": {
"commander": "^14.0.2",
"jsonwebtoken": "^9.0.2",
- "selenium-webdriver": "^4.30.0"
+ "selenium-webdriver": "latest"
}
},
"node_modules/@bazel/runfiles": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz",
- "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
+ "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
"license": "Apache-2.0"
},
"node_modules/buffer-equal-constant-time": {
@@ -99,12 +99,12 @@
}
},
"node_modules/jwa": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
- "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
+ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
- "buffer-equal-constant-time": "1.0.1",
+ "buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
@@ -203,16 +203,36 @@
"util-deprecate": "~1.0.1"
}
},
- "node_modules/safe-buffer": {
+ "node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/selenium-webdriver": {
- "version": "4.30.0",
- "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.30.0.tgz",
- "integrity": "sha512-3DGtQI/xyAg05SrqzzpFaXRWYL+Kku3fsikCoBaxApKzhBMUX5UiHdPb2je2qKMf2PjJiEFaj0L5xELHYRbYMA==",
+ "version": "4.38.0",
+ "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.38.0.tgz",
+ "integrity": "sha512-5/UXXFSQmn7FGQkbcpAqvfhzflUdMWtT7QqpEgkFD6Q6rDucxB5EUfzgjmr6JbUj30QodcW3mDXehzoeS/Vy5w==",
"funding": [
{
"type": "github",
@@ -227,17 +247,17 @@
"dependencies": {
"@bazel/runfiles": "^6.3.1",
"jszip": "^3.10.1",
- "tmp": "^0.2.3",
- "ws": "^8.18.0"
+ "tmp": "^0.2.5",
+ "ws": "^8.18.3"
},
"engines": {
- "node": ">= 18.20.5"
+ "node": ">= 20.0.0"
}
},
"node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -261,10 +281,16 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
"node_modules/tmp": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz",
- "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"license": "MIT",
"engines": {
"node": ">=14.14"
@@ -277,9 +303,9 @@
"license": "MIT"
},
"node_modules/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/tests/package.json b/tests/package.json
index a256efe..d06b076 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -2,6 +2,6 @@
"dependencies": {
"commander": "^14.0.2",
"jsonwebtoken": "^9.0.2",
- "selenium-webdriver": "^4.30.0"
+ "selenium-webdriver": "latest"
}
}
From c728a880245a4814516230bf539f48d647377c77 Mon Sep 17 00:00:00 2001
From: rcbjBlueMars <9321971+rcbjBlueMars@users.noreply.github.com>
Date: Fri, 7 Nov 2025 07:28:28 -0800
Subject: [PATCH 10/11] Refresh Token control and results display now works
when you return from token_detail or userinfo views. Userinfo has a backend
processing option and works correctly with POST method.
---
api/package.json | 2 +-
api/server.js | 144 +++++++++++++++++++++++++++++-------
client/public/debugger.html | 4 +-
client/public/userinfo.html | 10 +++
client/src/debugger2.js | 73 ++++++++++++++----
client/src/introspection.js | 12 ++-
client/src/token_detail.js | 5 +-
client/src/userinfo.js | 127 ++++++++++++++++++++++---------
common/common.sh | 25 ++++---
9 files changed, 306 insertions(+), 96 deletions(-)
diff --git a/api/package.json b/api/package.json
index 9fdd521..51da41f 100644
--- a/api/package.json
+++ b/api/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"description": "idptools client project",
- "author": "Robert C. Broeckelmann Jr. ",
+ "author": "Robert C. Broeckelmann Jr. ",
"scripts": {
"start": "node server.js"
},
diff --git a/api/server.js b/api/server.js
index 4d69db4..214a12b 100644
--- a/api/server.js
+++ b/api/server.js
@@ -1,8 +1,3 @@
-// File: server.js
-// Author: Robert C. Broeckelmann Jr.
-// Date: 05/31/2020
-// Notes:
-//
'use strict';
var appconfig = require(process.env.CONFIG_FILE);
@@ -20,17 +15,31 @@ const HOST = appconfig.host || '0.0.0.0';
const LOG_LEVEL = appconfig.logLevel || 'debug';
const uiUrl = appconfig.uiUrl || 'http://localhost:3000';
+const STATUS_200 = 200;
+const STATUS_400 = 400;
+const STATUS_401 = 401;
+const STATUS_403 = 403;
+const STATUS_404 = 404;
+const STATUS_500 = 500;
+
var log = bunyan.createLogger({ name: 'server',
level: LOG_LEVEL });
log.info("Log initialized. logLevel=" + log.level());
+var claimDescriptions = "";
+var cachedClaimDescriptions = false;
+
const app = express();
const expressSwagger = require('express-swagger-generator')(app);
app.use(bodyParser.json());
+var corsOptions = {
+ origin: '*',
+ optionsSuccessStatus: 204
+};
// app.use(expressLogging(logger));
-app.options('*', cors());
-app.use(cors());
+app.options("*", cors(corsOptions));
+app.use(cors(corsOptions));
/**
* @typedef HealthcheckResponse
@@ -45,7 +54,9 @@ app.use(cors());
* @returns {Error.model} 500 - Unexpected error
*/
app.get('/healthcheck', function (req, res) {
- res.json({ message: 'Success' });
+ res
+ .status(STATUS_200)
+ .json({ message: 'Success' });
});
/**
@@ -57,19 +68,31 @@ app.get('/healthcheck', function (req, res) {
* @returns {Error.model} 500 - Unexpected error
*/
app.get('/claimdescription', function(req, res) {
+ console.log("Entering GET /claimdescription.");
try {
- fetch("https://www.iana.org/assignments/jwt/jwt.xml")
- .then((response) => {
- response.text()
- .then( (text) => {
- log.debug("Retrieved: " + text);
- res
- .append('Content-Type', 'application/xml')
- .send(text)
- });
- })
- .catch(function (error) {
- log.error('Error from claimsdescription endpoint: ' + error);
+ if(cachedClaimDescriptions) {
+ console.debug("Using cached claim descriptions.");
+ res
+ .append('Content-Type', 'application/xml')
+ .status(STATUS_200)
+ .send(claimDescriptions);
+ } else {
+ log.debug("Pulling claim descriptions");
+ fetch("https://www.iana.org/assignments/jwt/jwt.xml")
+ .then((response) => {
+ response
+ .text()
+ .then( (text) => {
+ log.debug("Retrieved: " + text);
+ res
+ .append('Content-Type', 'application/xml')
+ .send(text);
+ cachedClaimDescriptions = true;
+ claimDescriptions = text;
+ });
+ })
+ .catch(function (error) {
+ log.error('Error from claimsdescription endpoint: ' + error.stack);
if(!!error.response) {
if(!!error.response.status) {
log.error("Error Status: " + error.response.status);
@@ -84,14 +107,15 @@ app.get('/claimdescription', function(req, res) {
res.status(error.response.status);
res.json(error.response.data);
} else {
- res.status(500);
+ res.status(STATUS_500);
res.json(error.message);
}
}
});
+ }
} catch(e) {
log.error("An error occurred while retrieving the claim description XML: " + e.stack);
- res.status(500)
+ res.status(STATUS_500)
.render('error', { error: e });
}
});
@@ -203,13 +227,13 @@ app.post('/token', (req, res) => {
});
} catch (e) {
log.error('An error occurred: ' + e);
- res.status(500);
+ res.status(STATUS_500);
res.json({ "error": e });
}
});
/**
- * @typedef IntrspectionRequest
+ * @typedef IntrospectionRequest
* @property {string} grant_type.required - The OAuth2 / OIDC Grant / Flow Type
* @property {string} client_id.required - The OAuth2 client identifier
*/
@@ -276,7 +300,7 @@ try {
res.status(error.response.status);
res.json(error.response.data);
} else {
- res.status(500);
+ res.status(STATUS_500);
res.json(error.message);
}
}
@@ -286,6 +310,75 @@ try {
}
});
+app.post('/userinfo', (req, res) => {
+ log.info('Entering app.post for /userinfo.');
+ userinfo_common(req, res);
+ log.debug("Leaving app.post for /userinfo.");
+});
+
+/**
+ * Wrapper around OIDC UserInfo Endpoint
+ * @route POST /userinfo
+ * @group Debugger - Operations for OAuth2/OIDC Debugger
+ * @param {UserInfoRequest.model} req.body.required - UserInfo Endpoint Request
+ * @returns {UserInfoResponse.model} 200 - UserInfo Endpoint Response
+ * @returns {Error.model} 400 - Syntax error
+ * @returns {Error.model} 500 - Unexpected error
+ */
+app.get('/userinfo', (req, res) => {
+ log.info("Entering app.get for /userinfo.");
+ userinfo_common(req, res);
+ log.debug("Leaving app.get for /userinfo.");
+});
+
+function userinfo_common(req, res) {
+try {
+ log.info('Entering app.get for /userinfo.');
+ var headers = {
+ "Authorization": req.headers.authorization,
+ };
+ // All types of requests are converted to GET.
+ log.debug("Method: GET");
+ log.debug("URL: " + Buffer.from(req.query.userinfo_endpoint, 'base64').toString('utf-8'));
+ log.debug("headers: " + JSON.stringify(headers));
+ axios({
+ method: 'get',
+ url: Buffer.from(req.query.userinfo_endpoint, 'base64').toString('utf-8'),
+ headers: headers,
+ httpsAgent: new (require('https').Agent)({ rejectUnauthorized: true })
+ })
+ .then(function (response) {
+ log.debug('Response from OIDC UserInfo Endpoint: ' + JSON.stringify(response.data));
+ log.debug('Headers: ' + response.headers);
+ res.status(response.status);
+ res.json(response.data);
+ })
+ .catch(function (error) {
+ log.error('Error from OIDC UserInfo Endpoint: ' + error);
+ if(!!error.response) {
+ if(!!error.response.status) {
+ log.error("Error Status: " + error.response.status);
+ }
+ if(!!error.response.data) {
+ log.error("Error Response body: " + JSON.stringify(error.response.data));
+ }
+ if(!!error.response.headers) {
+ log.error("Error Response headers: " + error.response.headers);
+ }
+ if (!!error.response) {
+ res.status(error.response.status);
+ res.json(error.response.data);
+ } else {
+ res.status(STATUS_500);
+ res.json(error.message);
+ }
+ }
+ });
+ } catch(e) {
+ log.error("Error from OIDC UserInfo Endpoint: " + error);
+ }
+}
+
let options = {
swaggerDefinition: {
info: {
@@ -305,7 +398,6 @@ let options = {
basedir: __dirname, //app absolute path
files: ['server.js'] //Path to the API handle folder
};
-
expressSwagger(options)
app.listen(PORT, HOST);
log.info(`Running on http://${HOST}:${PORT}`);
diff --git a/client/public/debugger.html b/client/public/debugger.html
index 35d992d..2d036c6 100644
--- a/client/public/debugger.html
+++ b/client/public/debugger.html
@@ -33,8 +33,10 @@
-This is a simple Client to use with any OAuth2 or OpenID Connect compliant identity provider..
+
This is a simple Client to use with any OAuth2 or OpenID Connect compliant identity provider.
+OAuth2 RFC
+OIDC Spec
This page manages interaction with the OAuth2 Authorization Endpoint.
OpenID Connect Discovery Endpoint Information
diff --git a/client/public/userinfo.html b/client/public/userinfo.html
index 241d2ff..316fec5 100644
--- a/client/public/userinfo.html
+++ b/client/public/userinfo.html
@@ -60,6 +60,16 @@
+
+
+ Initiate UserInfo Endpoint Call From front or backend.: The debugger can initiate an UserInfo Endpoint call from either the frontend (browser) or from the backend API component. Certain IdPs make stringent assumptions about CORS and how calls will be originated. Especially concerning the Origin request header, which cannot be controlled from the browser.
+
+
+ Front
+ Back
+
+
+