Skip to content

Adding JSON based authentication #36

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

Merged
merged 1 commit into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
253 changes: 253 additions & 0 deletions source/includes/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,3 +653,256 @@ with the Bodgeit application. Use the [includeInContext](#contextactionincludein
and use the [setAuthenticationMethod](#authenticationactionsetauthenticationmethod) to setup the authentication method and
the configuration parameters. Finally use the users API to create the admin user. Refer the script in the right column
on how to use the above APIs.

## JSON Based Authentication

```python

#!/usr/bin/env python
import urllib.parse
from zapv2 import ZAPv2

context_id = 1
apiKey = 'changeMe'
context_name = 'Default Context'
target_url = 'http://localhost:3000'

# By default ZAP API client will connect to port 8080
zap = ZAPv2(apikey=apiKey)

# Use the line below if ZAP is not listening on port 8080, for example, if listening on port 8090
# zap = ZAPv2(apikey=apiKey, proxies={'http': 'http://127.0.0.1:8090', 'https': 'http://127.0.0.1:8090'})


def set_include_in_context():
include_url = 'http://localhost:3000.*'
zap.context.include_in_context(context_name, include_url)
print('Configured include and exclude regex(s) in context')


def set_logged_in_indicator():
logged_in_regex = '\Q<a href="logout.php">Logout</a>\E'
logged_out_regex = '(?:Location: [./]*login\.php)|(?:\Q<form action="login.php" method="post">\E)'

zap.authentication.set_logged_in_indicator(context_id, logged_in_regex)
zap.authentication.set_logged_out_indicator(context_id, logged_out_regex)
print('Configured logged in indicator regex: ')


def set_json_based_auth():
login_url = "http://localhost:3000/rest/user/login"
login_request_data = 'email={%username%}&password={%password%}'

json_based_config = 'loginUrl=' + urllib.parse.quote(login_url) + '&loginRequestData=' + urllib.parse.quote(login_request_data)
zap.authentication.set_authentication_method(context_id, 'jsonBasedAuthentication', json_based_config)
print('Configured form based authentication')


def set_user_auth_config():
user = 'Test User'
username = '[email protected]'
password = 'testtest'

user_id = zap.users.new_user(context_id, user)
user_auth_config = 'username=' + urllib.parse.quote(username) + '&password=' + urllib.parse.quote(password)
zap.users.set_authentication_credentials(context_id, user_id, user_auth_config)


def add_script():
script_name = 'jwtScript.js'
script_type = 'HTTP Sender'
script_engine = 'Oracle Nashorn'
file_name = '/tmp/jwtScript.js'
zap.script.load(script_name, script_type, script_engine, file_name)


set_include_in_context()
add_script()
set_json_based_auth()
set_logged_in_indicator()
set_user_auth_config()
```

```java
public class JSONAuth {

private static final String ZAP_ADDRESS = "localhost";
private static final int ZAP_PORT = 8090;
private static final String ZAP_API_KEY = null;
private static final String contextId = "1";
private static final String target = "http://localhost:3000";

private static void setJSONBasedAuthentication(ClientApi clientApi) throws ClientApiException, UnsupportedEncodingException {
String loginUrl = "http://localhost:3000/rest/user/login";
String loginRequestData = "username={%username%}&password={%password%}";

// Prepare the configuration in a format similar to how URL parameters are formed. This
// means that any value we add for the configuration values has to be URL encoded.
StringBuilder jsonBasedConfig = new StringBuilder();
jsonBasedConfig.append("loginUrl=").append(URLEncoder.encode(loginUrl, "UTF-8"));
jsonBasedConfig.append("&loginRequestData=").append(URLEncoder.encode(loginRequestData, "UTF-8"));

System.out.println("Setting JSON based authentication configuration as: " + jsonBasedConfig.toString());
clientApi.authentication.setAuthenticationMethod(contextId, "jsonBasedAuthentication", jsonBasedConfig.toString());

// Check if everything is set up ok
System.out.println("Authentication config: " + clientApi.authentication.getAuthenticationMethod(contextId).toString(0));
}

private static String setUserAuthConfig(ClientApi clientApi) throws ClientApiException, UnsupportedEncodingException {
// Prepare info
String user = "Test User";
String username = "[email protected]";
String password = "testtest";

// Make sure we have at least one user
String userId = extractUserId(clientApi.users.newUser(contextId, user));

// Prepare the configuration in a format similar to how URL parameters are formed. This
// means that any value we add for the configuration values has to be URL encoded.
StringBuilder userAuthConfig = new StringBuilder();
userAuthConfig.append("username=").append(URLEncoder.encode(username, "UTF-8"));
userAuthConfig.append("&password=").append(URLEncoder.encode(password, "UTF-8"));

System.out.println("Setting user authentication configuration as: " + userAuthConfig.toString());
clientApi.users.setAuthenticationCredentials(contextId, userId, userAuthConfig.toString());
clientApi.users.setUserEnabled(contextId, userId, "true");
clientApi.forcedUser.setForcedUser(contextId, userId);
clientApi.forcedUser.setForcedUserModeEnabled(true);

// Check if everything is set up ok
System.out.println("Authentication config: " + clientApi.users.getUserById(contextId, userId).toString(0));
return userId;
}

private static void addScript(ClientApi clientApi) throws ClientApiException {

String script_name = "jwtScript.js";
String script_type = "HTTP Sender";
String script_engine = "Oracle Nashorn";
String file_name = "/tmp/authscript.js";

clientApi.script.load(script_name, script_type, script_engine, file_name, null);
}

private static void scanAsUser(ClientApi clientApi, String userId) throws ClientApiException {
clientApi.spider.scanAsUser(contextId, userId, target, null, "true", null);
}

private static String extractUserId(ApiResponse response) {
return ((ApiResponseElement) response).getValue();
}

/**
* The main method.
*
* @param args the arguments
* @throws ClientApiException
* @throws UnsupportedEncodingException
*/
public static void main(String[] args) throws ClientApiException, UnsupportedEncodingException {
ClientApi clientApi = new ClientApi(ZAP_ADDRESS, ZAP_PORT, ZAP_API_KEY);

addScript(clientApi);
setJSONBasedAuthentication(clientApi);
String userId = setUserAuthConfig(clientApi);
scanAsUser(clientApi, userId);
}
}
```

```shell

# To add the script
curl 'http://localhost:8080/JSON/script/action/load/?scriptName=authscript.js&scriptType=authentication&scriptEngine=Oracle+Nashorn&fileName=%2Ftmp%2Fauthscript.js&scriptDescription=&charset=UTF-8'

# To set up authentication information
curl 'http://localhost:8080/JSON/authentication/action/setAuthenticationMethod/?contextId=1&authMethodName=scriptBasedAuthentication&authMethodConfigParams=scriptName%3Dauthscript.js%26Login+URL%3Dhttp%3A%2F%2Flocalhost%3A3000%2Flogin.php%26CSRF+Field%3Duser_token%26POST+Data%3Dusername%3D%7B%25username%25%7D%26password%3D%7B%25password%25%7D%26Login%3DLogin%26user_token%3D%7B%25user_token%25%7D'

# To set the login indicator
curl 'http://localhost:8080/JSON/authentication/action/setLoggedInIndicator/?contextId=1&loggedInIndicatorRegex=%5CQ%3Ca+href%3D%22logout.jsp%22%3ELogout%3C%2Fa%3E%5CE'

# To create a user (The first user id is: 0)
curl 'http://localhost:8080/JSON/users/action/newUser/?contextId=1&name=Test+User'

# To add the credentials for the user
curl 'http://localhost:8080/JSON/users/action/setAuthenticationCredentials/?contextId=1&userId=0&authCredentialsConfigParams=username%3Dtest%40example.com%26password%3DweakPassword'

# To enable the user
curl 'http://localhost:8080/JSON/users/action/setUserEnabled/?contextId=1&userId=0&enabled=true'

# To set forced user
curl 'http://localhost:8080/JSON/forcedUser/action/setForcedUser/?contextId=1&userId=0'

# To enable forced user mode
curl 'http://localhost:8080/JSON/forcedUser/action/setForcedUserModeEnabled/?boolean=true'
```

The following example performs a script based authentication for the OWASP Juice Shop. Juice Shop is a modern application and
it contrary to the previous examples the protected resources are accessed by sending an authorization header(JSON web token).

### Setup Target Application

Use the following docker command to start the OWASP Juice Shop.

`docker run -d -p 3000:3000 bkimminich/juice-shop`

### Register User

Register a user in the application by navigating to the following URL: [http://localhost:3000/#/register](http://localhost:3000/#/register).
For the purpose of this example, use the following information.

* Email: [email protected]
* Password: testtest
* Security Question: Select Your eldest siblings middle name (enter any text)

### Login

After registering the user, browse (proxied via ZAP) to the following URL ([http://localhost:3000/#/login](http://localhost:3000/#/login))
and login to the application. When you login to the application the request will be added to the `History` tab in ZAP.
Search for the POST request to the following URL: [http://localhost:3000/rest/user/login](http://localhost:3000/rest/user/login).
Right-click on the POST request, and select `Flag as Context -> Default Context : JSON-based Auth Login Request` option. This will open the context authentication editor.
You can notice it has auto selected the JSON-based authentication, auto-filled the login URL and the post data.
Select the correct JSON attribute as the username and password in the dropdown and click Ok. The following image shows the completed setup for the authentication tab of the context menu.

![json based authentication](../images/auth_json.png)

Exit the context editor and go back to the login request. You will notice in the login response headers there is no set cookie. In
the response body you will find the response data.

The request that follows is GET http://localhost:3000/rest/user/whoami which you will notice has a header called Authorization
which uses the token from the response body of the login request. In body of the response, you should see some info about your
user: `{"user":{"id":1,"email":"[email protected]"}}`. If you visit that url directly, with your browser, the content of the page is
`{"user":{}}` - the Authorization header is not added to request and it is not authenticated.

This request is initiated as a client side AJAX request using a spec called JWT. Currently ZAP doesn't have a notion of
the Authorization header for sessions so this is where ZAPs scripting engine will come into play! With ZAP's scripting
engine, we can easily add to or augment it's functionality.
Copy link
Member

@kingthorin kingthorin May 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe these 3 lines are addressed by new functionality that psiinon implemented before his long ADDO session.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psiinon where can I get the link for this session? :O

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workshop section 7 or 8 I think.

https://www.alldaydevops.com/zap-in-ten

To be clear I wanna as talking about session management scripts.

https://github.com/zaproxy/zaproxy/tree/develop/zap/src/main/dist/scripts/templates/session

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the text is technically correct for 2.9.


### Add the Script

Now in the left sidebar next to the Sites click + to add Scripts. This will bring into focus in the sidebar. Drill into
`Scripting > Scripts > HTTP Sender`. Then right click on the HTTP Sender and with that context menu click New Script. Name
the script `jwtScript.js` and set the Script Engine to ECMAScript (do not check the box that says enable).

![json authentication script](../images/auth_json_script.png)

Now that we have that script setup, let's test it out! Go ahead and visit the login page http://localhost:3000/#/login
with the browser launched with ZAP and use your test account to login. After you login, back in ZAP in the Script Console
tab you should see a message that says `Capturing token for JWT`.

Now visit http://localhost:3000/rest/user/whoami directly in the browser and you will see you get JSON data with the
user `{"user":{"id":9,"email":"[email protected]"}}`! Back in the Script Console you will see the script went ahead and added the header!

Now that we have a script ensuring we have the right headers & cookies for authentication, let's go ahead and try
spidering the application again! So let's use the same settings we used earlier from the AJAX Spider [Settings](#AJAX Spider).
Once the scan starts, check out the browser running the scan - you'll notice the user is logged in! (Logout & Your Basket links visible).
Now the AJAX Spider will pick up some new paths that it couldn't find before!

### Steps to Reproduce via API

Use the scripts endpoint to add the script file. Thereafter the configurations are very similar to the form based authentication
with the Bodgeit application. Use the [includeInContext](#contextactionincludeincontext) API to add the URL to the default context
and use the [setAuthenticationMethod](#authenticationactionsetauthenticationmethod) to setup the authentication method and
the configuration parameters. Finally use the users API to create the admin user. Refer the script in the right column
on how to use the above APIs.
68 changes: 68 additions & 0 deletions source/scripts/jwtScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Logging with the script name is super helpful!
function logger() {
print('[' + this['zap.script.name'] + '] ' + arguments[0]);
}

// Control.getSingleton().getExtensionLoader().getExtension(ExtensionUserManagement.class);
var HttpSender = Java.type('org.parosproxy.paros.network.HttpSender');
var ScriptVars = Java.type('org.zaproxy.zap.extension.script.ScriptVars');
var HtmlParameter = Java.type('org.parosproxy.paros.network.HtmlParameter')
var COOKIE_TYPE = org.parosproxy.paros.network.HtmlParameter.Type.cookie;

function sendingRequest(msg, initiator, helper) {
if (initiator === HttpSender.AUTHENTICATION_INITIATOR) {
logger("Trying to auth")
return msg;
}

var token = ScriptVars.getGlobalVar("jwt-token")
if (!token) {return;}
var headers = msg.getRequestHeader();
var cookie = new HtmlParameter(COOKIE_TYPE, "token", token);
msg.getRequestHeader().getCookieParams().add(cookie);
// For all non-authentication requests we want to include the authorization header
logger("Added authorization token " + token.slice(0, 20) + " ... ")
msg.getRequestHeader().setHeader('Authorization', 'Bearer ' + token);
return msg;
}

function responseReceived(msg, initiator, helper) {
var resbody = msg.getResponseBody().toString()
var resheaders = msg.getResponseHeader()

if (initiator !== HttpSender.AUTHENTICATION_INITIATOR) {
var token = ScriptVars.getGlobalVar("jwt-token");
if (!token) {return;}

var headers = msg.getRequestHeader();
var cookies = headers.getCookieParams();
var cookie = new HtmlParameter(COOKIE_TYPE, "token", token);

if (cookies.contains(cookie)) {return;}
msg.getResponseHeader().setHeader('Set-Cookie', 'token=' + token + '; Path=/;');
return;
}

logger("Handling auth response")
if (resheaders.getStatusCode() > 299) {
logger("Auth failed")
return;
}

// Is response JSON? @todo check content-type
if (resbody[0] !== '{') {return;}
try {
var data = JSON.parse(resbody);
} catch (e) {
return;
}

// If auth request was not succesful move on
if (!data['authentication']) {return;}

// @todo abstract away to be configureable
var token = data["authentication"]["token"]
logger("Capturing token for JWT\n" + token)
ScriptVars.setGlobalVar("jwt-token", token)
msg.getResponseHeader().setHeader('Set-Cookie', 'token=' + token + '; Path=/;');
}