diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e9adb54..079d070 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,9 +13,11 @@ 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: 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 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 c2e5551..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,11 +54,13 @@ 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' }); }); /** - * System healthcheck + * Retrieve Claims Description. * @route GET /claimdescription * @group Metadata - Support operations * @returns {HealthcheckResponse.model} 200 - Claim Description Response @@ -57,16 +68,56 @@ 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); + console.log("Entering GET /claimdescription."); + try { + if(cachedClaimDescriptions) { + console.debug("Using cached claim descriptions."); res .append('Content-Type', 'application/xml') - .send(text) + .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); + } + 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("An error occurred while retrieving the claim description XML: " + e.stack); + res.status(STATUS_500) + .render('error', { error: e }); + } }); /** @@ -176,11 +227,158 @@ 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 IntrospectionRequest + * @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) => { +try { + 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(STATUS_500); + res.json(error.message); + } + } + }); + } catch(e) { + log.error("Error from OAuth2 Introspection Endpoint: " + error); + } +}); + +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: { @@ -200,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..551bf40 100644 --- a/client/public/debugger.html +++ b/client/public/debugger.html @@ -21,11 +21,11 @@ @@ -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 @@ -47,7 +49,7 @@ - + @@ -159,7 +161,7 @@ @@ -169,7 +171,7 @@ @@ -179,7 +181,7 @@ @@ -189,7 +191,7 @@ @@ -357,11 +359,11 @@ diff --git a/client/public/debugger2.html b/client/public/debugger2.html index 5a6dfa4..001fc0f 100644 --- a/client/public/debugger2.html +++ b/client/public/debugger2.html @@ -21,11 +21,11 @@ @@ -33,7 +33,7 @@ -

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..

This page manages interaction with the OAuth2 Token Endpoint.

@@ -111,7 +111,7 @@ @@ -120,8 +120,8 @@ @@ -131,7 +131,7 @@ @@ -166,7 +166,7 @@
Exchange Authorization Code for Access Token -
+
@@ -424,7 +424,7 @@
@@ -452,7 +452,7 @@
-
+
 

An OIDC Discovery endpoint uses a path that ends in /.well-known/openid-configuration. See the spec.

An OIDC Discovery endpoint uses a path that ends in /.well-known/openid-configuration. See the spec.

@@ -149,7 +151,7 @@ - Review JWKS meta data + Review JWKS meta data
Yes No - +
Yes No - +
Yes No - +
Yes No - +
- Review JWKS meta data + Review JWKS meta data
Yes - No - + No +
Yes No - +
- +  
@@ -494,7 +494,7 @@ @@ -508,11 +508,11 @@ diff --git a/client/public/introspection.html b/client/public/introspection.html index 49d0b8a..7f56c6a 100644 --- a/client/public/introspection.html +++ b/client/public/introspection.html @@ -21,11 +21,11 @@ @@ -48,6 +48,16 @@ + + + + @@ -149,9 +159,9 @@ diff --git a/client/public/jwks.html b/client/public/jwks.html index ead9a0e..8d5d159 100644 --- a/client/public/jwks.html +++ b/client/public/jwks.html @@ -21,11 +21,11 @@ @@ -40,7 +40,7 @@
- +  
+
The debugger can initiate an Introspection 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 + +
The OIDC Introspection Token. @@ -130,7 +140,7 @@
- Return to debugger + Return to debugger
-
Return to debugger + Return to debugger
@@ -70,9 +70,9 @@ diff --git a/client/public/logout.html b/client/public/logout.html index d39abde..dd9a505 100644 --- a/client/public/logout.html +++ b/client/public/logout.html @@ -21,11 +21,11 @@ @@ -39,7 +39,7 @@ -
Return to debugger + Return to debugger

Logout successful

@@ -50,9 +50,9 @@ diff --git a/client/public/privacypolicy.html b/client/public/privacypolicy.html index 1744908..2ae455f 100644 --- a/client/public/privacypolicy.html +++ b/client/public/privacypolicy.html @@ -47,14 +47,14 @@

Privacy Policy

-

Privacy Policy for rcbj.net

-

The privacy of our visitors to rcbj.net is important to us.

-

At rcbj.net, we recognize that privacy of your personal information is important. Here is information on what types of personal information we receive and collect when you use and visit rcbj.net, and how we safeguard your information. We never sell your personal information to third parties.

+

Privacy Policy for idp.tools

+

The privacy of our visitors to idp.tools is important to us.

+

At idp.tools, we recognize that privacy of your personal information is important. Here is information on what types of personal information we receive and collect when you use and visit idp.tools, and how we safeguard your information. We never sell your personal information to third parties.

Log Files

As with most other websites, we collect and use the data contained in log files. The information in the log files include your IP (internet protocol) address, your ISP (internet service provider, such as AOL or Shaw Cable), the browser you used to visit our site (such as Internet Explorer or Firefox), the time you visited our site and which pages you visited throughout our site.

Cookies and Web Beacons

We do use cookies to store information, such as your personal preferences when you visit our site. This could include only showing you a popup once in your visit, or the ability to login to some of our features, such as forums.

-

We also use third party advertisements on rcbj.net to support our site. Some of these advertisers may use technology such as cookies and web beacons when they advertise on our site, which will also send these advertisers (such as Google through the Google AdSense program) information including your IP address, your ISP , the browser you used to visit our site, and in some cases, whether you have Flash installed. This is generally used for geotargeting purposes (showing New York real estate ads to someone in New York, for example) or showing certain ads based on specific sites visited (such as showing cooking ads to someone who frequents cooking sites).

+

We also use third party advertisements on idp.tools to support our site. Some of these advertisers may use technology such as cookies and web beacons when they advertise on our site, which will also send these advertisers (such as Google through the Google AdSense program) information including your IP address, your ISP , the browser you used to visit our site, and in some cases, whether you have Flash installed. This is generally used for geotargeting purposes (showing New York real estate ads to someone in New York, for example) or showing certain ads based on specific sites visited (such as showing cooking ads to someone who frequents cooking sites).

You can chose to disable or selectively turn off our cookies or third-party cookies in your browser settings, or by managing preferences in programs such as Norton Internet Security. However, this can affect how you are able to interact with our site as well as other websites. This could include the inability to login to services or programs, such as logging into forums or account.

To summarize:

    diff --git a/client/public/token_detail.html b/client/public/token_detail.html index d887a8a..ac184f0 100644 --- a/client/public/token_detail.html +++ b/client/public/token_detail.html @@ -22,11 +22,11 @@ @@ -44,15 +44,15 @@ Decoded Token
    - - + +
    @@ -122,7 +122,7 @@ @@ -193,9 +193,9 @@
    diff --git a/client/public/userinfo.html b/client/public/userinfo.html index 241d2ff..e0b7052 100644 --- a/client/public/userinfo.html +++ b/client/public/userinfo.html @@ -21,11 +21,11 @@ @@ -60,6 +60,16 @@ + + + + @@ -127,9 +137,9 @@ diff --git a/client/src/debugger.js b/client/src/debugger.js index a603a08..2bf77f6 100644 --- a/client/src/debugger.js +++ b/client/src/debugger.js @@ -1,8 +1,3 @@ -// File: debugger.js -// Author: Robert C. Broeckelmann Jr. -// Date: 06/15/2017 -// Notes: -// var appconfig = require(process.env.CONFIG_FILE); var bunyan = require("bunyan"); var DOMPurify = require("dompurify"); @@ -1494,6 +1489,13 @@ function getLSBooleanItem(key) return localStorage.getItem(key) === 'true'; } +function clickLink() { + log.debug("Entering clickLink()."); + writeValuesToLocalStorage(); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { OnSubmitForm, OnSubmitTokenEndpointForm, @@ -1523,5 +1525,6 @@ module.exports = { onClickShowAuthzFieldSet, onClickShowGenericFieldSet, onClickClearLocalStorage, - usePKCERFC + usePKCERFC, + clickLink }; diff --git a/client/src/debugger2.js b/client/src/debugger2.js index 9b676a1..a3600a5 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"); @@ -18,6 +14,7 @@ var currentRefreshToken = ''; var usePKCE = true; var useFrontEnd = false; var useRefreshFrontEnd = false; +var refreshTokenUsed = false; function OnSubmitTokenEndpointForm() { @@ -38,56 +35,28 @@ function getParameterByName(name, url) return urlParams.get(name); } -$(document).ready(function() { - log.debug("Entering ready function()."); - // Call original onload function - onload(); - var sel = $("#authorization_grant_type"); - sel.change(function() { - log.debug("Entering selection changed function()."); +function logoutButtonClick() { + log.debug("Logout link clicked."); + var nameValuePairs = {}; + + $('#logout_fieldset input.q').each(function() { + var className = $(this).attr('name'); var value = $(this).val(); - localStorage.setItem("authorization_grant_type", value); - if (value != "client_credential") { - writeValuesToLocalStorage(); - window.location.href = "/debugger.html"; + if (value!=""){ + nameValuePairs[className] = value; } - resetUI(value); - recalculateTokenRequestDescription(); - recalculateRefreshRequestDescription(); - log.debug("Leaving selection changed function()."); }); - var value = $("#authorization_grant_type").val(); - resetUI(value); - recalculateRefreshRequestDescription(); - - $("#logout_btn").click(function() { - log.debug("Logout link clicked."); - var nameValuePairs = {}; + log.debug(nameValuePairs); // Log the name-value pairs + var queryString = $.param(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); - - log.debug(queryString); // Log the query string - var logoutUrl = DOMPurify.sanitize($("#logout_end_session_endpoint").val()) + "?" + DOMPurify.sanitize(queryString); - - clearLocalStorage(); - window.location.href = logoutUrl; - - return false; - }); + log.debug(queryString); // Log the query string + var logoutUrl = DOMPurify.sanitize($("#logout_end_session_endpoint").val()) + "?" + DOMPurify.sanitize(queryString); - $(".token_btn").click(tokenButtonClick); - $(".refresh_btn").click(refreshButtonClick); + clearLocalStorage(); + window.location.href = logoutUrl; - log.debug("Leaving token submit button clicked function."); -}); + return false; +}; function tokenButtonClick() { log.debug("Entering token Submit button clicked function."); @@ -151,7 +120,6 @@ function buildInternalTokenAPIRequestMessage() { } var auth_style = getLSBooleanItem("token_post_auth_style"); - log.debug("RCBJ0001: " + auth_style); var formData = {}; if(grant_type == "authorization_code") { @@ -240,13 +208,14 @@ function successfulInternalTokenAPICall(data, textStatus, request) if(displayOpenIDConnectArtifacts == true) { // Display OAuth2/OIDC Artifacts + log.debug("RCBJ0003"); token_endpoint_result_html = "
    " + "Token Endpoint Results:" + "
    - Return to debugger + Return to debugger
    - Return to debugger + Return to debugger
    +
    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 + +
    The OIDC UserInfo Endpoint Scope. @@ -108,7 +118,7 @@
    - Return to debugger + Return to debugger
    " + "" + '' + @@ -259,8 +228,8 @@ function successfulInternalTokenAPICall(data, textStatus, request) if(useRefreshTokenTester) { token_endpoint_result_html += '' + '' + @@ -273,8 +242,8 @@ function successfulInternalTokenAPICall(data, textStatus, request) } token_endpoint_result_html += "" + '' + @@ -291,12 +260,13 @@ 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:" + "
    ' + - '

    Access Token

    ' + - '

    Introspect Token

    ' + + '

    Access Token

    ' + + '

    Introspect Token

    ' + '

    ' + '
    ' + - '

    Refresh Token

    ' + - '

    Introspect Token

    ' + + '

    Refresh Token

    ' + + '

    Introspect Token

    ' + '

    ' + '
    ' + - '

    ID Token

    ' + - '

    Get UserInfo Data

    ' + + '

    ID Token

    ' + + '

    Get UserInfo Data

    ' + '

    ' + '
    " + "" + '' + @@ -309,7 +279,7 @@ function successfulInternalTokenAPICall(data, textStatus, request) log.debug("Refresh token found. Generating token: data.refresh_token=" + currentRefreshToken); token_endpoint_result_html += "" + '' + @@ -327,6 +297,7 @@ function successfulInternalTokenAPICall(data, textStatus, request) //$("#token_endpoint_result").html(DOMPurify.sanitize(token_endpoint_result_html)); $("#token_endpoint_result").html(token_endpoint_result_html); $("#refresh_refresh_token").val(currentRefreshToken); + log.debug("RCBJ0200"); $("#refresh_client_id").val(localStorage.getItem("client_id")); $("#refresh_scope").val(localStorage.getItem("scope")); $("#refresh_client_secret").val(localStorage.getItem("client_secret")); @@ -431,40 +402,72 @@ function successfulInternalRefreshAPICall(data, textStatus, request) { + textStatus + ", request=" + JSON.stringify(request)); - var refresh_endpoint_result_html = ""; log.debug("displayOpenIDConnectArtifacts=" + displayOpenIDConnectArtifacts); - var iteration = 1; - if(!!$("#refresh-token-results-iteration-count").val()) - { - iteration = parseInt($("#refresh-token-results-iteration-count").val()) + 1; - } + refreshTokenUsed=true; + localStorage.setItem("refresh_token_used", true); + var currentRefreshToken = ""; + var currentAccessToken = ""; + var currentIDToken = ""; log.debug('data.refresh_token=' + data.refresh_token); + log.debug("data.access_token=" + data.access_token); + log.debug("data.id_token=" + data.id_token); if(!!data.refresh_token) { log.debug('Setting new Refresh Token.'); currentRefreshToken = data.refresh_token; } - if(displayOpenIDConnectArtifacts == true) + if(!!data.access_token) { + log.debug("Setting new Access Token."); + currentAccessToken = data.access_token; + } + if(!!data.id_token) { + log.debug("Setting new ID Token."); + currentIDToken = data.id_token; + } + recreateRefreshTokenDisplay(currentRefreshToken, currentAccessToken, currentIDToken); + log.debug("Leaving ajax success function for Refresh Token."); +} + +function recreateRefreshTokenDisplay(currentRefreshToken, currentAccessToken, currentIDToken) { + log.debug("Entering displayRefreshTokenPane()."); + var refresh_endpoint_result_html = ""; + log.debug("displayOpenIDConnectArtifacts=" + displayOpenIDConnectArtifacts); + var iteration = 0; + if(!!localStorage.getItem("refresh_iteration")) { - refresh_endpoint_result_html = "
    " + + //iteration = parseInt($("#refresh-token-results-iteration-count").val()) + 1; + iteration = parseInt(localStorage.getItem("refresh_iteration")) + 1; + } + localStorage.setItem("refresh_iteration", iteration); + if (!!!currentRefreshToken) { + currentRefreshToken = localStorage.getItem("refresh_refresh_token"); + } + if (!!!currentAccessToken) { + currentAccessToken = localStorage.getItem("refresh_access_token"); + } + if (!!!currentIDToken) { + currentIDToken = localStorage.getItem("refresh_id_token"); + } + refresh_endpoint_result_html = "
    " + "Token Endpoint Results for Refresh Token Call:" + "
    ' + - '

    Access Token

    ' + + '

    Access Token

    ' + '

    ' + '
    ' + - 'Refresh Token' + + 'Refresh Token' + '

    ' + '
    " + "" + '" + "" + - "" + - "" + + ""; + if(!!currentRefreshToken) { + refresh_endpoint_result_html += "" + '" + @@ -472,21 +475,24 @@ function successfulInternalRefreshAPICall(data, textStatus, request) { currentRefreshToken + "" + "" + - "" + - "" + + ""; + } + if(displayOpenIDConnectArtifacts) { + refresh_endpoint_result_html += "" + '" + "" + - "" + - "" + + ""; + } + refresh_endpoint_result_html += "" + "" + "
    ' + - '

    Access Token

    ' + - '

    Introspect Token

    ' + + '

    Latest Access Token

    ' + + '

    Introspect Token

    ' + '

    ' + "
    " + "" + "
    ' + - '

    Refresh Token

    ' + - '

    Introspect Token

    ' + + '

    Latest Refresh Token

    ' + + '

    Introspect Token

    ' + '

    ' + "
    ' + - '

    ID Token

    ' + - '

    Get UserInfo Data

    ' + + '

    Latest ID Token

    ' + + '

    Get UserInfo Data

    ' + '

    ' + "
    " + "" + "
    iteration" + '" + "
    " + ""; - } else { - refresh_endpoint_result_html = "
    " + - "Token Endpoint Results for Refresh Token Call:" + - "" + - "" + - "" + - "" + - "" + - "" + - '' + - "" + - "" + - "" + - "" + - "" + - "" + - "
    " + - 'Access Token' + - '

    ' + - "
    " + - "" + - "
    ' + - 'ID Token' + - '

    ' + - '
    " + - "
    iteration" - '' - "
    " + - "
    "; - } //$("#refresh_endpoint_result").html(DOMPurify.sanitize(refresh_endpoint_result_html)); $("#refresh_endpoint_result").html(refresh_endpoint_result_html); // Update refresh token field in the refresh token grant pane $("#refresh_refresh_token").val(currentRefreshToken); // Store new tokens in local storage - localStorage.setItem("refresh_access_token", data.access_token ); - localStorage.setItem("refresh_refresh_token", currentRefreshToken ); - localStorage.setItem("refresh_id_token", data.id_token ); + if (!!currentAccessToken) { + localStorage.setItem("refresh_access_token", currentAccessToken ); + } + if (!!currentRefreshToken) { + localStorage.setItem("refresh_refresh_token", currentRefreshToken ); + } + if (!!currentIDToken) { + localStorage.setItem("refresh_id_token", currentIDToken); + } // Update token in logout pane. if(currentRefreshToken) { - $("#logout_id_token_hint").val(data.id_token); + $("#logout_id_token_hint").val(currentIDToken); } else { $("#logout_fieldset").hide(); } recalculateRefreshRequestDescription(); + if (refreshTokenUsed) { + $("#refresh_endpoint_result").show(); + } else { + $("#refresh_endpoint_result").hide(); + } + log.debug("Leaving displayRefreshTokenPane()."); } function errorInternalRefreshAPICall(request, status, error) { @@ -605,6 +586,23 @@ 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(); + } + 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); @@ -690,24 +688,32 @@ function writeValuesToLocalStorage() localStorage.setItem("token_initiateFromBackEnd", $("#token_initiateFromBackEnd").is(":checked")); localStorage.setItem("refresh_initiateFromFrontEnd", $("#refresh_initiateFromFrontEnd").is(":checked")); localStorage.setItem("refresh_initiateFromBackEnd", $("#refresh_initiateFromBackEnd").is(":checked")); + localStorage.setItem("refresh_token_used", refreshTokenUsed); } 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")); @@ -738,7 +744,7 @@ function loadValuesFromLocalStorage() $("#refresh_initiateFromBackEnd").prop("checked", getLSBooleanItem("refresh_initiateFromBackEnd")); $("#refresh_refresh_token").val(localStorage.getItem("refresh_refresh_token")); - $("#refresh_client_id").val(localStorage.getItem("refresh_client_id")); + $("#customTokenParametersCheck-no").prop("checked", getLSBooleanItem("customTokenParametersCheck-no"));$("#refresh_client_id").val(localStorage.getItem("refresh_client_id")); $("#refresh_scope").val(localStorage.getItem("refresh_scope")); $("#refresh_client_secret").val(localStorage.getItem("refresh_client_secret")); $("#useRefreshToken-yes").prop("checked", getLSBooleanItem("useRefreshToken_yes")); @@ -778,7 +784,13 @@ function loadValuesFromLocalStorage() $("#token_pkce_code_method").val(localStorage.getItem("PKCE_code_challenge_method")); } usePKCERFC(); + refreshTokenUsed=getLSBooleanItem("refresh_token_used"); + 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); @@ -792,47 +804,65 @@ 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); } } - 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); 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) { - access_token = "NO_ACCESS_TOKEN_PRESENTED_IN_EXPECTED_LOCATIONS"; - } - } - log.debug("access_token=" + access_token); + 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:" + "" + "" + - "" + - "" + "" + "
    access_token" + "
    " + "
    "; $("#authorization_endpoint_result").html(DOMPurify.sanitize(authorization_endpoint_result_html)); + 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. @@ -841,7 +871,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()); @@ -888,7 +918,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("
    " + @@ -935,12 +965,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" + + "
    " + + "" + + "" + + "" + + "" + + "" + + "
    " + + "" + + "" + + "" + "
    " + + "
    " + + "
    "; + $("#display_authz_error_class").html(DOMPurify.sanitize(error_html)); } - log.debug("Leaving loadValuesFromLocalStorage()."); + log.debug("Entering recreateUniqueGrantFlowElements()."); } function recalculateTokenRequestDescription() @@ -1052,49 +1101,94 @@ function recalculateRefreshRequestDescription() log.debug("Leaving recalculateRefreshRequestDescription()."); } -function onload() { - log.debug("Entering onload function."); +function processStateParameter() +{ + log.debug("Entering 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(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()."); +} + +$(document).ready(function() { + log.debug("Entering document.ready() function."); if (!appconfig) { log.debug('Failed to load appconfig.'); } + + var authorization_grant_type = $("#authorization_grant_type").val(); + + $("#authorization_grant_type").change(function() { + log.debug("Entering selection changed function()."); + var value = $(this).val(); + localStorage.setItem("authorization_grant_type", value); + if (value != "client_credential") { + writeValuesToLocalStorage(); + window.location.href = "/debugger.html"; + } + resetUI(value); + recalculateTokenRequestDescription(); + recalculateRefreshRequestDescription(); + log.debug("Leaving selection changed function()."); + }); $("#password-form-group1").hide(); $("#password-form-group2").hide(); + // If we are not 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", ""); localStorage.setItem("refresh_access_token", ""); localStorage.setItem("refresh_id_token", ""); localStorage.setItem("refresh_refresh_token", ""); + localStorage.setItem("refresh_iteration", ""); } - // 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'); log.debug('errorDescriptionParam=' + errorDescriptionParam + ', errorParam=' + errorParam); - if (errorDescriptionParam || errorParam) { + if (errorDescriptionParam || + errorParam) { $('#step0').hide(); $('#step3').hide(); $('#step4').hide(); @@ -1106,6 +1200,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(); @@ -1114,6 +1214,7 @@ function onload() { $("#customTokenParametersCheck-no").on("click", recalculateTokenRequestDescription); loadValuesFromLocalStorage(); + recreateUniqueGrantFlowElements(); recalculateAuthorizationErrorDescription(); recalculateTokenRequestDescription(); recalculateRefreshRequestDescription(); @@ -1164,15 +1265,27 @@ function onload() { displayTokenCustomParametersCheck(); - if(getParameterByName("redirectFromTokenDetail") == "true") { + if( getParameterByName("redirectFromTokenDetail") == "true" && + ( authorization_grant_type != "implicit_grant" && + authorization_grant_type != "oidc_implicit_grant")) + { log.debug('Detected redirect back from token detail page.'); $("#step3").hide(); + if (useRefreshTokenTester) { + $("#step4").show(); + } recreateTokenDisplay(); + recreateRefreshTokenDisplay("", "", ""); // no new token $("#logout_id_token_hint").val(localStorage.getItem("token_id_token")); } - log.debug("Leaving onload()."); -} + recalculateRefreshRequestDescription(); + + $(".token_btn").click(tokenButtonClick); + $(".refresh_btn").click(refreshButtonClick); + + log.debug("Leaving document.ready()."); +}); function generateUUID () { // Public Domain/MIT log.debug("Entering generateUUID()."); @@ -1511,13 +1624,14 @@ function recreateTokenDisplay() { log.debug("Displaying full OIDC Token results."); // Display OAuth2/OIDC Artifacts + log.debug("RCBJ0001"); token_endpoint_result_html = "
    " + "Token Endpoint Results:" + "" + "" + '" + @@ -1531,8 +1645,8 @@ function recreateTokenDisplay() log.debug("Displaying refresh token."); token_endpoint_result_html += '' + '' + @@ -1545,8 +1659,8 @@ function recreateTokenDisplay() } token_endpoint_result_html += "" + '' + @@ -1561,12 +1675,13 @@ function recreateTokenDisplay() } else { log.debug("Logging access_token only."); + log.debug("RCBJ0002"); token_endpoint_result_html = "
    " + "Token Endpoint Results:" + "
    ' + - '

    Access Token

    ' + - '

    Introspect Token

    ' + + '

    Access Token

    ' + + '

    Introspect Token

    ' + '

    ' + "
    ' + - '

    Refresh Token

    ' + - '

    Introspect Token

    ' + + '

    Refresh Token

    ' + + '

    Introspect Token

    ' + '

    ' + '
    ' + - '

    ID Token

    ' + - '

    Get UserInfo Data

    ' + + '

    ID Token

    ' + + '

    Get UserInfo Data

    ' + '

    ' + '
    " + "" + '' + @@ -1579,7 +1694,7 @@ function recreateTokenDisplay() log.debug("Displaying refresh token"); token_endpoint_result_html += "" + '' + @@ -1692,6 +1807,30 @@ function initFields() { log.debug("Entering initFields()."); var token_initialize = getLSBooleanItem("token_initialize"); if(!token_initialize) { + if ($("#yesCheckOIDCArtifacts")) { + $("#yesCheckOIDCArtifacts").prop("checked", true); + } + if ($("#noCheckOIDCArtifacts")) { + $("#noCheckOIDCArtifacts").prop("checked", false); + } + if ($("#SSLValidate-yes")) { + $("#SSLValidate-yes").prop("checked", true); + } + if ($("#SSLValidate-no")) { + $("#SSLValidate-no").prop("checked", false); + } + if ($("#useRefreshToken-yes")) { + $("#useRefreshToken-yes").prop("checked", true); + } + if ($("#useRefreshToken-no")) { + $("#useRefreshToken-no").prop("checked", false); + } + if ($("#usePKCE-yes")) { + $("#usePKCE-yes").prop("checked", true); + } + if ($("#usePKCE-no")) { + $("#usePKCE-no").prop("checked", false); + } if ($("#yesResourceCheckToken")) { $("#yesResourceCheckToken").prop("checked", false); localStorage.setItem("yesResourceCheckToken", false); @@ -1720,6 +1859,25 @@ function initFields() { if ($("#refresh_headerAuthStyleCheckToken")) { $("#refresh_headerAuthStyleCheckToken").prop("checked", false); } + if ($("#usePKCE-yes")) { + $("#usePKCE-yes").prop("checked", true); + } + if ($("#usePKCE-no")) { + $("#usePKCE-no").prop("checked", false); + } + if ($("#token_initiateFromFrontEnd")) { + $("#token_initiateFromFrontEnd").prop("checked", true); + } + if ($("#token_initiateFromBackEnd")) { + $("#token_initiateFromBackEnd").prop("checked", false); + } + if ($("#refresh_initiateFromFrontEnd")) { + $("#refresh_initiateFromFrontEnd").prop("checked", true); + } + if ($("#refresh_initiateFromBackEnd")) { + $("#refresh_initiateFromFrontEnd").prop("checked", false); + } + localStorage.setItem("refresh_post_auth_style", true); localStorage.setItem("token_initialize", true); token_initialize = true; @@ -1795,7 +1953,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; @@ -1829,6 +1986,13 @@ function setInitiateRefreshFromEnd() { log.debug("Leaving setInitiateRefreshFromEnd()."); } +function clickLink() { + log.debug("Entering clickLink()."); + writeValuesToLocalStorage(); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { OnSubmitTokenEndpointForm, getParameterByName, @@ -1861,5 +2025,7 @@ module.exports = { setHeaderAuthStyleRefreshToken, onClickCopyToken, setInitiateFromEnd, - setInitiateRefreshFromEnd + setInitiateRefreshFromEnd, + logoutButtonClick, + clickLink }; diff --git a/client/src/introspection.js b/client/src/introspection.js index a60822c..0e1758d 100644 --- a/client/src/introspection.js +++ b/client/src/introspection.js @@ -1,14 +1,11 @@ -// File: introspection.js -// Author: Robert C. Broeckelmann Jr. -// Date: 07/11/2020 -// Notes: -// var appconfig = require(process.env.CONFIG_FILE); var bunyan = require("bunyan"); 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 +28,77 @@ 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_ = ""; + log.debug("Making introspection call. useFrontEnd=" + useFrontEnd); + if(useFrontEnd) { + url_ = introspection_endpoint; + } else { + url_ = appconfig.apiUrl + "/introspection"; + body["introspectionEndpoint"] = 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 +124,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 +135,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()."); } @@ -138,10 +169,46 @@ 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("useFrontEnd=" + useFrontEnd + ", typeof(useFrontEnd)=" + typeof(useFrontEnd)); log.debug("Leaving window.onload() function."); } +function setInitiateFromEnd(which_end) { + log.debug("Entering setInitiateFromEnd(). which_end=" + which_end); + var frontEndInitiated = $("#introspection_initiateFromFrontEnd").is(":checked"); + var backEndInitiated = $("#introspection_initiateFromBackEnd").is(":checked"); + log.debug("typeof(frontEndInitiated): " + typeof(frontEndInitiated)); + 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'; +} + +function clickLink() { + log.debug("Entering clickLink()."); + writeValuesToLocalStorage(); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { callIntrospectionEndpoint, - onClickToggleConfigurationParameters -}; \ No newline at end of file + onClickToggleConfigurationParameters, + setInitiateFromEnd, + clickLink +}; diff --git a/client/src/jwks.js b/client/src/jwks.js index d0f3902..b11d7cf 100644 --- a/client/src/jwks.js +++ b/client/src/jwks.js @@ -33,7 +33,6 @@ function loadValuesFromLocalStorage() function OnSubmitJWKSEndpointForm() { log.debug("Entering OnSubmitJWKSEndpointForm()."); -// writeValuesToLocalStorage(); var jwksEndpoint = document.getElementById("jwks_endpoint").value; log.debug('URL: ' + jwksEndpoint); if (isUrl(jwksEndpoint)) { @@ -167,6 +166,12 @@ function onSubmitClearAllForms() { $("#jwks_info_table").html(""); } +function clickLink() { + log.debug("Entering clickLink()."); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { loadValuesFromLocalStorage, OnSubmitJWKSEndpointForm, @@ -176,4 +181,5 @@ module.exports = { parseJWKSInfo, buildJWKSInfoTable, onSubmitPopulateFormsWithDiscoveryInformation, + clickLink }; diff --git a/client/src/logout.js b/client/src/logout.js index 7777efe..3a5582d 100644 --- a/client/src/logout.js +++ b/client/src/logout.js @@ -1,8 +1,3 @@ -// File: logout.js -// Author: Robert C. Broeckelmann Jr. -// Date: 12/28/2024 -//Notes: -// var appconfig = require(process.env.CONFIG_FILE); var bunyan = require("bunyan"); var $ = require("jquery"); @@ -18,6 +13,13 @@ function loadValuesFromLocalStorage() log.debug("Entering loadValuesFromLocalStorage()."); } +function clickLink() { + log.debug("Entering clickLink()."); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { - loadValuesFromLocalStorage, + loadValuesFromLocalStorage, + clickLink }; diff --git a/client/src/token_detail.js b/client/src/token_detail.js index eaa7bc4..c3756a3 100644 --- a/client/src/token_detail.js +++ b/client/src/token_detail.js @@ -186,7 +186,7 @@ window.onload = function() { } else if (type == 'refresh_access') { jwt = localStorage.getItem("refresh_access_token"); } else if (type == 'refresh_refresh') { - jwt = localStorage("refresh_refresh_token"); + jwt = localStorage.getItem("refresh_refresh_token"); } else if (type == 'refresh_id') { jwt = localStorage.getItem('refresh_id_token'); } else { @@ -270,6 +270,9 @@ window.onload = function() { keyPairJWTPayload += '
    ' + - '

    Access Token

    ' + + '

    Access Token

    ' + '

    ' + '
    ' + - 'Refresh Token' + + 'Refresh Token' + '

    ' + '
    '; $('#key_pair_jwt_payload').html(keyPairJWTPayload); }); + }) + .catch( (error) => { + log.error("An error was encountered: " + error.stack); }); } @@ -287,8 +290,16 @@ function populateTable(evt, tabName) { evt.currentTarget.className += " active"; } +function clickLink() { + log.debug("Entering clickLink()."); + writeValuesToLocalStorage(); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { decodeJWT, verifyJWT, - populateTable + populateTable, + clickLink }; diff --git a/client/src/userinfo.js b/client/src/userinfo.js index 8acbf00..03d86c5 100644 --- a/client/src/userinfo.js +++ b/client/src/userinfo.js @@ -1,8 +1,3 @@ -// File: userinfo.js -// Author: Robert C. Broeckelmann Jr. -// Date: 07/11/2020 -// Notes: -// var appconfig = require(process.env.CONFIG_FILE); var bunyan = require("bunyan"); var $ = require("jquery"); @@ -18,6 +13,8 @@ var userinfo_claims = ""; var token_access_token = ""; var query_string = ""; +var useFrontEnd = false; + function getParameterByName(name, url) { log.debug("Entering getParameterByName()."); @@ -36,57 +33,93 @@ window.onload = function() initLocalStorage(); loadValuesFromLocalStorage(); resetErrorDisplays(); + var frontEndInitiated = $("#userinfo_initiateFromFrontEnd").is(":checked"); + if(frontEndInitiated) { + useFrontEnd = true; + } else { + useFrontEnd = false; + } + log.debug("useFrontEnd=" + useFrontEnd + ", typeof(useFrontEnd)=" + typeof(useFrontEnd)); + recalculateUserInfoURL(); log.debug("Leaving window.onload() function."); } function recalculateUserInfoURL() { log.debug("Entering recalculateUserInfoURL() function."); - if(userinfo_scope) { + if(!!userinfo_scope) { query_string = 'scope=' + userinfo_scope; } - if(userinfo_claims) { + if(!!userinfo_claims) { query_string += '&claims=' + userinfo_claims; } - log.debug("Leaving recalcualteUserInfoURL()."); + query_string = encodeURI(query_string); + log.debug("Leaving recalcualteUserInfoURL(): query_string=" + query_string); } function callUserInfoEndpoint() { log.debug("Entering callUserInfoEndpoint()."); - var userinfoEndpointCall = $.ajax({ + var url_ = ""; + log.debug("Making Userinfo call. useFrontEnd=" + useFrontEnd); + const headers = { + "Authorization": 'Bearer ' + token_access_token, + }; + if ( userinfo_method === "post" ) { + headers["Content-Type"] = "application/json"; + } + if(useFrontEnd) { + url_ = userinfo_endpoint + "?" + query_string; + } else { + url_ = appconfig.apiUrl + + "/userinfo?" + + query_string + + "&userinfo_endpoint=" + + Buffer.from(userinfo_endpoint + + "?" + + query_string).toString('base64'); + } + log.debug("url_: " + url_); + $.ajax({ type: userinfo_method, crossdomain: true, - url: userinfo_endpoint + "?" + query_string, - headers: { - Authorization: 'Bearer ' + token_access_token - }, - success: function(data, textStatus, request) { - log.debug('Entering ajax success function for Access Token call.'); - log.debug('UserInfo textStatus: ' + JSON.stringify(textStatus)); - log.debug('UserInfo Endpoint Response: ' + JSON.stringify(data)); - log.debug('UserInfo request: ' + JSON.stringify(request)); - log.debug('UserInfo Response Content-Type: ' + userinfoEndpointCall.getResponseHeader("Content-Type")); - log.debug('UserInfo Headers: ' + JSON.stringify(userinfoEndpointCall.getAllResponseHeaders())); - var responseContentType = userinfoEndpointCall.getResponseHeader("Content-Type"); - if (responseContentType.includes('application/json')) { - log.debug('plaintext response detected, no signature, no encryption'); - log.debug('UserInfo Endpoint Response: ' + JSON.stringify(data, null, 2)); - document.getElementById("userinfo_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)); - // recalculateTokenErrorDescription(request); - } + url: url_, + headers: headers, + success: ajaxSuccessFunction, + error: ajaxErrorFunction }); log.debug("Entering callUserInfoEndpoint()."); } +function ajaxSuccessFunction(data, textStatus, jqXHR) { + log.debug('Entering ajax success function for Access Token call.'); + log.debug('UserInfo textStatus: ' + JSON.stringify(textStatus)); + log.debug('UserInfo Endpoint Response: ' + JSON.stringify(data)); + log.debug('UserInfo request: ' + JSON.stringify(jqXHR)); + log.debug('UserInfo Response Content-Type: ' + jqXHR.getResponseHeader("Content-Type")); + log.debug('UserInfo 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('UserInfo Endpoint Response: ' + JSON.stringify(data, null, 2)); + $("#userinfo_output").val(JSON.stringify(data,null,2)); + } else { + log.error('Unknown response format.'); + } +} + +function ajaxErrorFunction(request, status, error) { + log.debug("request: " + JSON.stringify(request)); + log.debug("status: " + JSON.stringify(status)); + log.debug("error: " + JSON.stringify(error)); + const errorStatus = { + request: request, + status: status, + error: error + }; + $("#userinfo_output").val(JSON.stringify(errorStatus,null,2)); +} + $(".userinfo_endpoint").keypress(function() { log.debug("Entering keypress()."); localStorage.setItem("userinfo_endpoint", userinfo_endpoint); @@ -202,8 +235,37 @@ function onClickToggleConfigurationParameters() { log.debug("Leaving onClickToggleConfigurationParameters()."); } +function setInitiateFromEnd(which_end) { + log.debug("Entering setInitiateFromEnd(). which_end=" + which_end); + var frontEndInitiated = $("#userinfo_initiateFromFrontEnd").is(":checked"); + var backEndInitiated = $("#userinfo_initiateFromBackEnd").is(":checked"); + log.debug("typeof(frontEndInitiated): " + typeof(frontEndInitiated)); + 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'; +} + +function clickLink() { + log.debug("Entering clickLink()."); + writeValuesToLocalStorage(); + log.debug("Leaving clickLink()."); + return true; +} + module.exports = { getParameterByName, callUserInfoEndpoint, - onClickToggleConfigurationParameters + onClickToggleConfigurationParameters, + setInitiateFromEnd, + clickLink }; diff --git a/common/common.sh b/common/common.sh new file mode 100755 index 0000000..f1b6dfb --- /dev/null +++ b/common/common.sh @@ -0,0 +1,407 @@ +#!/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 OIDC_AUTHORIZATION_CODE_CONFIDENTIAL OIDC_AUTHORIZATION_CODE_PUBLIC + 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'", + "access.token.lifespan": 3600 + } + }' + 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'", + "access.token.lifespan": 3600 + } + }' + ;; + 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'", + "access.token.lifespan": 3600 + } + }' + 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'", + "access.token.lifespan": 3600 + } + }' + 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'", + "access.token.lifespan": 3600 + } + }' + 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 -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}" + 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 + 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()." +} + +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} \ + SCOPE=${CLIENT_CREDENTIALS_SCOPE} \ + node ${NODEJS_BASE_DIR}/oauth2_client_credentials.js --url "${DEBUGGER_BASE_URL}" + check_return_code $? + + # 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}" + echo "SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE}" + echo "USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER}" + 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} \ + SCOPE=${AUTHORIZATION_CODE_CONFIDENTIAL_SCOPE} \ + USER=${AUTHORIZATION_CODE_CONFIDENTIAL_USER} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + 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}" + echo "SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE}" + echo "USER=${AUTHORIZATION_CODE_PUBLIC_USER}" + 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} \ + SCOPE=${AUTHORIZATION_CODE_PUBLIC_SCOPE} \ + USER=${AUTHORIZATION_CODE_PUBLIC_USER} \ + PKCE_ENABLED=${PKCE_ENABLED} \ + node ${NODEJS_BASE_DIR}/oauth2_authorization_code.js --url "${DEBUGGER_BASE_URL}" + check_return_code $? + done + + # OAuth2 Implicit Grant + AUDIENCE=${IMPLICIT_AUDIENCE} \ + DISCOVERY_ENDPOINT=${IMPLICIT_DISCOVERY_ENDPOINT} \ + CLIENT_ID=${IMPLICIT_CLIENT_ID} \ + SCOPE=${IMPLICIT_SCOPE} \ + USER=${IMPLICIT_USER} \ + 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/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 1ff427b..3c8f4ad 100755 --- a/local-run-tests.sh +++ b/local-run-tests.sh @@ -1,133 +1,50 @@ #!/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 + NODEJS_BASE_DIR=tests } -init -npm install --prefix tests -# Install testing dependencies +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 "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') -if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ]; -then - echo "Unable to obtain tokens." - 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 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://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') - if [ -z "${KEYCLOAK_ACCESS_TOKEN}" ]; - then - echo "Unable to obtain tokens." - 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"}}' - 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"}' - 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"}}' - 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"}}' - 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}" - 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}" ]; - then - echo "USER_ID 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}' - check_return_code $? - - declare ${FLOW_VARIABLE}_DISCOVERY_ENDPOINT="http://localhost:8080/realms/debugger-testing/.well-known/openid-configuration" - declare ${FLOW_VARIABLE}_CLIENT_ID="${CLIENT_CLIENTID}" - declare ${FLOW_VARIABLE}_CLIENT_SECRET="${CLIENT_SECRET}" - declare ${FLOW_VARIABLE}_SCOPE="${SCOPE_NAME}" - declare ${FLOW_VARIABLE}_USER="${USER_ID}" -done - +configureKeycloak +check_return_code $? +runTests +check_return_code $? node --version -# 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 - # 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}" --browser - check_return_code $? - - # 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 - 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: 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 && \ 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."); diff --git a/tests/oauth2_implicit.js b/tests/oauth2_implicit.js new file mode 100644 index 0000000..ac5e08f --- /dev/null +++ b/tests/oauth2_implicit.js @@ -0,0 +1,276 @@ +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 logout_post_redirect_uri_value = baseUrl + "/logout.html"; +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 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) { + 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("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/oidc_authorization_code.js b/tests/oidc_authorization_code.js new file mode 100644 index 0000000..6a516a9 --- /dev/null +++ b/tests/oidc_authorization_code.js @@ -0,0 +1,563 @@ +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"; + } else if ( type == "refresh_access_token") { + token_field = "refresh_access_token"; + link_text == "Latest Access Token"; + } else if ( type == "refresh_refresh_token") { + token_field = "refresh_refresh_token"; + link_text = "Latest Refresh Token"; + } else if ( type == "refresh_id_token" ) { + token_field = "refresh_id_token"; + link_text = "Latest 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(2000); + 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 refresh_token_call(driver, client_id, scope, user, access, audience) { + console.log("Entering refresh_token_call()."); + console.log("Find Refresh Button"); + refresh_btn = By.id("refresh_btn"); + console.log("Locate refresh_btn."); + await driver.wait(until.elementLocated(refresh_btn), 10000); + console.log("Wait for refresh_btn to be visible."); + await driver.wait(until.elementIsVisible(driver.findElement(refresh_btn)), 10000); + console.log("Click refresh_btn. Making refresh token call."); + await driver.findElement(refresh_btn).click(); + console.log("Waiting for call to complete."); + await wait(4000); + console.log("Finding refresh_access_token."); + var refresh_access_token = By.id("refresh_access_token"); + console.log("Locate refresh_access_token."); + var refresh_access_token_element = await driver.wait(until.elementLocated(refresh_access_token), 10000); + console.log("Waiting for refresh_access_token to be visible."); + await driver.wait(until.elementIsVisible(driver.findElement(refresh_btn)), 10000); + var refresh_access_token_value = await driver.findElement(refresh_access_token).getAttribute("value"); + + console.log("Calling verifyAccessToken()."); + await verifyAccessToken(refresh_access_token_value, client_id, scope, user, access, audience); + + var refresh_refresh_token = By.id("refresh_refresh_token"); + console.log("Locate refresh_refresh_token."); + await driver.wait(until.elementLocated(refresh_refresh_token), 10000); + console.log("Waiting for refresh_refresh_token to be visible."); + await driver.wait(until.elementIsVisible( driver.findElement(refresh_refresh_token)), 10000); + var refresh_refresh_token_value = await driver.findElement(refresh_refresh_token).getAttribute("value"); + + console.log("Calling verifyRefreshToken()."); + await verifyRefreshToken(refresh_refresh_token_value, client_id, user, audience, audience); + + var refresh_id_token = By.id("refresh_id_token"); + console.log("Locate refresh_id_token."); + await driver.wait(until.elementLocated(refresh_id_token), 10000); + console.log("Waiting for refresh_id_token to be visible."); + await driver.wait(until.elementIsVisible( driver.findElement(refresh_id_token)), 10000); + var refresh_id_token_value = await driver.findElement(refresh_id_token).getAttribute("value"); + + console.log("Calling verifyIDToken()."); + await verifyIDToken(refresh_id_token_value, client_id, user, client_id, audience) + + console.log("Leaving refresh_token_call()."); +} + +async function logout(driver) { + console.log("Entering logout()."); + console.log("Find logout Button"); + 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); + logout_button = By.id("logout_btn"); + console.log("Locate logout_button."); + await driver.wait(until.elementLocated(logout_button), 10000); + console.log("Waiting for logout_button to be visible."); + await driver.wait(until.elementIsVisible(driver.findElement(logout_button)), 10000); + 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("Locate client_id"); + await driver.wait(until.elementLocated(client_id), 10000); + 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("Making refresh_token_call()."); + await refresh_token_call(driver, client_id, scope, user, "account", audience); + console.log("Go to refresh_access_token detail page."); + await tokenDetailPage(driver, "refresh_access_token"); +// console.log("Go to refresh_refresh_token detail page."); +// await tokenDetailPage(driver, "refresh_refresh_token"); +// console.log("Go to refresh_id_token detail page."); +// await tokenDetailPage(driver, "refresh_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" } }