Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Ory Kratos authentication scripts #447

Merged
merged 7 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- passive/JavaDisclosure.js - Passive scan for Java error messages leaks
- httpsender/RsaEncryptPayloadForZap.py - A script that encrypts requests using RSA
- selenium/FillOTPInMFA.js - A script that fills the OTP in MFA
- authentication/KratosApiAuthentication.js - A script to authenticate with Kratos using the API flow
- authentication/KratosBrowserAuthentication.js - A script to authenticate with Kratos using the browser flow

### Changed
- Use Prettier to format all JavaScript scripts.
Expand Down
135 changes: 135 additions & 0 deletions authentication/KratosApiAuthentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* This script is used to authenticate a user using Ory Kratos self-service API for clients without browsers.
*
* Authentication Verification can be configured with the following rule:
* - Verification Strategy: Poll the Specified URL
* - Regex Pattern used to identify Logged In message: \Qactive.*
thc202 marked this conversation as resolved.
Show resolved Hide resolved
* - URL to poll: <Kratos Base URL>/sessions/whoami
*
* Zap must be configured to use HTTP header-based session management with the value: {%json:session_token%}
thc202 marked this conversation as resolved.
Show resolved Hide resolved
*
* @author Edouard Maleix <[email protected]>
* @see https://www.ory.sh/docs/kratos/self-service/flows/user-login#login-for-api-clients-and-clients-without-browsers
*/

const HttpRequestHeader = Java.type(
"org.parosproxy.paros.network.HttpRequestHeader"
);
const HttpHeader = Java.type("org.parosproxy.paros.network.HttpHeader");
const URI = Java.type("org.apache.commons.httpclient.URI");
const AuthenticationHelper = Java.type(
"org.zaproxy.zap.authentication.AuthenticationHelper"
);

/**
* @typedef {Object} AuthHelper
* @property {function(): Object} prepareMessage - Prepares an HTTP message.
* @property {function(Object, boolean=): void} sendAndReceive - Sends the HTTP message and receives the response.
*/

/**
* @typedef {Object} ParamsValues
* @property {function(string): string} get - Gets the value of a parameter.
* @property {function(string): void} set - Sets the value of a parameter.
*/

/**
* @typedef {Object} Credentials
* @property {function(string): string} getParam - Gets the value of a parameter. The param names are the ones returned by the getCredentialsParamsNames() below
*/

/**
* @param {AuthHelper} helper - The authentication helper object provided by ZAP.
* @param {ParamsValues} paramsValues - The map of parameter values configured in the Session Properties - Authentication panel.
* @param {Credentials} credentials - an object containing the credentials values, as configured in the Session Properties - Users panel.
* @returns {Object} The HTTP message used to perform the authentication.
*/
function authenticate(helper, paramsValues, credentials) {
print("Authenticating via Ory Kratos...");

// Step 1: Initialize the login flow
const kratosBaseUri = paramsValues.get("Kratos Base URL");
const initLoginUri = new URI(
kratosBaseUri + "/self-service/login/api",
false
thc202 marked this conversation as resolved.
Show resolved Hide resolved
);
const initLoginMsg = helper.prepareMessage();
initLoginMsg.setRequestHeader(
new HttpRequestHeader(
HttpRequestHeader.GET,
initLoginUri,
HttpHeader.HTTP11
)
);
print("Sending GET request to " + initLoginUri);
helper.sendAndReceive(initLoginMsg);
print(
"Received response status code: " +
initLoginMsg.getResponseHeader().getStatusCode()
);
AuthenticationHelper.addAuthMessageToHistory(initLoginMsg);

// Step 2: Submit login credentials
const actionUrl = JSON.parse(initLoginMsg.getResponseBody().toString()).ui
.action;
const loginUri = new URI(actionUrl, false);
const loginMsg = helper.prepareMessage();
const requestBody = JSON.stringify({
method: "password",
identifier: credentials.getParam("username"),
password: credentials.getParam("password"),
});
loginMsg.setRequestBody(requestBody);

const requestHeader = new HttpRequestHeader(
HttpRequestHeader.POST,
loginUri,
HttpHeader.HTTP11
);
loginMsg.setRequestHeader(requestHeader);

// Build the POST request header
loginMsg
.getRequestHeader()
.setHeader(HttpHeader.CONTENT_TYPE, "application/json");
loginMsg
.getRequestHeader()
.setContentLength(loginMsg.getRequestBody().length());

print("Sending POST request to " + loginUri);
helper.sendAndReceive(loginMsg, false);
print(
"Received response status code: " +
loginMsg.getResponseHeader().getStatusCode()
);
AuthenticationHelper.addAuthMessageToHistory(loginMsg);

return loginMsg;
}

/**
* Returns the required parameter names.
*
* @returns {Array<string>} An array of required parameter names.
*/
function getRequiredParamsNames() {
return ["Kratos Base URL"];
}

/**
* Returns the optional parameter names.
*
* @returns {Array<string>} An array of optional parameter names.
*/
function getOptionalParamsNames() {
return [];
}

/**
* Returns the credentials parameter names.
*
* @returns {Array<string>} An array of credentials parameter names.
*/
function getCredentialsParamsNames() {
return ["username", "password"];
}
182 changes: 182 additions & 0 deletions authentication/KratosBrowserAuthentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* @description This script is used to authenticate a user using Ory Kratos self-service API for browser.
*
* Authentication Verification can be configured with the following rule:
* - Verification Strategy: Poll the Specified URL
* - Regex Pattern used to identify Logged In message: \Qactive.*
* - URL to poll: <Kratos Base URL>/sessions/whoami
*
* Zap must be configured to use cookie-based session management.
*
* @author Edouard Maleix <[email protected]>
* @see https://www.ory.sh/docs/kratos/self-service/flows/user-login#login-for-server-side-browser-clients
*/

const HttpRequestHeader = Java.type(
"org.parosproxy.paros.network.HttpRequestHeader"
);
const HttpHeader = Java.type("org.parosproxy.paros.network.HttpHeader");
const URI = Java.type("org.apache.commons.httpclient.URI");
const AuthenticationHelper = Java.type(
"org.zaproxy.zap.authentication.AuthenticationHelper"
);
const Source = Java.type("net.htmlparser.jericho.Source");

/**
* @typedef {Object} AuthHelper
* @property {function(): Object} prepareMessage - Prepares an HTTP message.
* @property {function(Object, boolean=): void} sendAndReceive - Sends the HTTP message and receives the response.
*/

/**
* @typedef {Object} ParamsValues
* @property {function(string): string} get - Gets the value of a parameter.
* @property {function(string): void} set - Sets the value of a parameter.
*/

/**
* @typedef {Object} Credentials
* @property {function(string): string} getParam - Gets the value of a parameter. The param names are the ones returned by the getCredentialsParamsNames() below
*/

/**
* @param {AuthHelper} helper - The authentication helper object provided by ZAP.
* @param {ParamsValues} paramsValues - The map of parameter values configured in the Session Properties - Authentication panel.
* @param {Credentials} credentials - an object containing the credentials values, as configured in the Session Properties - Users panel.
* @returns {Object} The HTTP message used to perform the authentication.
*/
function authenticate(helper, paramsValues, credentials) {
print("Authenticating via Ory Kratos...");

// Step 1: Initialize the login flow
const kratosBaseUri = paramsValues.get("Kratos Base URL");
const initLoginUri = new URI(
kratosBaseUri + "/self-service/login/browser",
false
);
const initLoginMsg = helper.prepareMessage();
initLoginMsg.setRequestHeader(
new HttpRequestHeader(
HttpRequestHeader.GET,
initLoginUri,
HttpHeader.HTTP11
)
);
print("Sending GET request to " + initLoginUri);
helper.sendAndReceive(initLoginMsg, true);
print(
"Received response status code: " +
initLoginMsg.getResponseHeader().getStatusCode()
);
AuthenticationHelper.addAuthMessageToHistory(initLoginMsg);

// Step 2: Submit login credentials
const actionUrl = getActionurl(initLoginMsg);
const loginUri = new URI(actionUrl, false);
const loginMsg = helper.prepareMessage();
const csrf_token = getCsrfToken(initLoginMsg);
const requestBody =
"identifier=" +
encodeURIComponent(credentials.getParam("username")) +
"&password=" +
encodeURIComponent(credentials.getParam("password")) +
"&method=password" +
"&csrf_token=" +
encodeURIComponent(csrf_token);
loginMsg.setRequestBody(requestBody);
const requestHeader = new HttpRequestHeader(
HttpRequestHeader.POST,
loginUri,
HttpHeader.HTTP11
);
loginMsg.setRequestHeader(requestHeader);

loginMsg
.getRequestHeader()
.setHeader(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
loginMsg
.getRequestHeader()
.setContentLength(loginMsg.getRequestBody().length());

print("Sending POST request to " + loginUri);
//! disable redirect to get the cookies from Kratos
helper.sendAndReceive(loginMsg, false);
print(
"Received response status code: " +
loginMsg.getResponseHeader().getStatusCode()
);
AuthenticationHelper.addAuthMessageToHistory(loginMsg);

return loginMsg;
}

/**
*
* @param {*} request
* @returns
*/
function getActionurl(request) {
let iterator;
let element;
let actionUrl;
const pageSource =
request.getResponseHeader().toString() +
request.getResponseBody().toString();
const src = new Source(pageSource);
const elements = src.getAllElements("form");

for (iterator = elements.iterator(); iterator.hasNext(); ) {
element = iterator.next();
actionUrl = element.getAttributeValue("action");
break;
}

return actionUrl;
}

function getCsrfToken(request) {
let iterator;
let element;
let loginToken;
const pageSource =
request.getResponseHeader().toString() +
request.getResponseBody().toString();
const src = new Source(pageSource);
const elements = src.getAllElements("input");

for (iterator = elements.iterator(); iterator.hasNext(); ) {
element = iterator.next();
if (element.getAttributeValue("name") == "csrf_token") {
loginToken = element.getAttributeValue("value");
break;
}
}

return loginToken;
}
/**
* Returns the required parameter names.
*
* @returns {Array<string>} An array of required parameter names.
*/
function getRequiredParamsNames() {
return ["Kratos Base URL"];
}

/**
* Returns the optional parameter names.
*
* @returns {Array<string>} An array of optional parameter names.
*/
function getOptionalParamsNames() {
return [];
}

/**
* Returns the credentials parameter names.
*
* @returns {Array<string>} An array of credentials parameter names.
*/
function getCredentialsParamsNames() {
return ["username", "password"];
}