Owasp Juice Shop Standard Authentication in ZAP

Owasp Juice Shop Standard Authentication in ZAP

This post offers an alternative to Section 6 Standard Auth in Juice Shop Zap in Ten video implementing standard authentication in ZAP for Juice Shop web application (https://owasp.org/www-project-juice-shop/). 

Prerequisites:

  • Juice Shop up and running
  • Java 1.8 or higher
  • Owasp Zap 2.9 (python scripting addon installed from marketplace)

Overview

Juice Shop represents modern day applications which is usually javascript/javascript based libraries heavy, e.g. Angular, React etc, on the front end, making ajax calls to the server to get data in the form of json or xml. As such, usual authentication methods that uses log in/out indicators work only during authentication but outside of that, while the ajax spider is running through the app, it usually will not know whether it is logged in or out based on the responses coming from the server unless the web application is consistent in the way it sends back responses when it is logged in/out. In this example of Juice Shop, after a user logs in, the user will get a token in return and use it for every single http call in the the Authorization header and also as a cookie. There is also no token expiry information when we get the token after logging in. There is also another endpoint /whoami that we can call to validate the token. We will be making use of a HttpSender script that will do these checks for every single request. This approach might look like an overhead but there are ways we can optimize it in your own PWA or SPA if there is an expiry value to the token (which we will normally have in most cases) where we check the expiry of the token before making a http call to validate the token or we have a timer that will validate the token after a certain time interval has elapsed.

For those who want to get straight to the HttpSender script, here it is in jpython:

import json
import org.parosproxy.paros.network.HttpRequestHeader as HttpRequestHeader
import org.parosproxy.paros.network.HttpHeader as HttpHeader
import org.zaproxy.zap.extension.script.ScriptVars as GlobalVariables
import org.parosproxy.paros.network.HttpMessage as HttpMessage
import org.parosproxy.paros.network.HtmlParameter as HtmlParameter
from org.apache.commons.httpclient import URI
from synchronize import make_synchronized
 
# base url of the juice shop web app
# TODO: change this to the url of your Juice Shop
BASE_URL = 'http://localhost:3000';
 
# function called for every outgoing request to juice shop (part of httpsender)
def sendingRequest(msg, initiator, helper):
    print('sendingRequest called for url=' + msg.getRequestHeader().getURI().toString())
 
    #checking if we already have an access token
    accessToken = GlobalVariables.getGlobalVar("accessToken");    
    if accessToken is not None:
        print "we have access token, checking if token is valid";
        if isTokenValid(accessToken, helper) == True:
            print "accessToken in valid";
            setAccessTokenInHttpMessage(accessToken, msg);
            return;
 
    print "token is invalid or there is no token, need to relogin"
    accessToken = refreshAccessToken(helper);
    setAccessTokenInHttpMessage(accessToken, msg);
    return
 
def isTokenValid(accessToken, helper):
    whoamiUrl = BASE_URL + "/rest/user/whoami";    
    returnedMsg = callGet(helper, whoamiUrl, accessToken);
    userJsonObject = json.loads(str(returnedMsg.getResponseBody()));
    userId = userJsonObject.get("user").get("id");
    print "userId:"+str(userId);
    if userId is None:
        return False;
 
    return True;
 
def doLogin(helper):
    loginUrl = BASE_URL + "/rest/user/login";
 
    # TODO: change the email and password to yours
    loginBody = "{\"email\":\"test@email.com\",\"password\":\"password\"}";
    returnedMsg = callPost(helper, loginUrl, loginBody, "application/json");
    accessToken = json.loads(str(returnedMsg.getResponseBody()))["authentication"]["token"];
    print "after doing login, we get accessToken:"+str(accessToken);
    return accessToken;
 
# function called for every incoming server response from juice shop (part of httpsender)
def responseReceived(msg, initiator, helper):
    print('responseReceived called for url=' +
          msg.getRequestHeader().getURI().toString())
 
# function to make generic GET request to Juice Shop
def callGet(helper, requestUrl, accessToken):
    requestUri = URI(requestUrl, False);
    print "-----start of callGet-------";
    print "requestUrl:"+requestUrl;
    msg = HttpMessage();
    requestHeader = HttpRequestHeader(HttpRequestHeader.GET, requestUri, HttpHeader.HTTP10);
    msg.setRequestHeader(requestHeader);
    setAccessTokenInHttpMessage(accessToken, msg);
    print "Sending GET request: " + str(requestHeader);
    helper.getHttpSender().sendAndReceive(msg)
    print "Received response status code for authentication request: " + str(msg.getResponseHeader());
    print "------------------------------------";
    return msg;
 
# function to make generic POST request to Juice Shop
def callPost(helper, requestUrl, requestBody, contentType):
    print "-----start of callPost ("+requestUrl+")-------";
 
    requestUri = URI(requestUrl, False);
    print "requestBody:"+ requestBody;
    msg = HttpMessage();
    requestHeader = HttpRequestHeader(HttpRequestHeader.POST, requestUri, HttpHeader.HTTP10);
    requestHeader.setHeader("content-type",contentType);
 
    msg.setRequestHeader(requestHeader);
    msg.setRequestBody(requestBody);
    print("Sending POST request header: " + str(requestHeader));
    print("Sending POST request body: " + str(requestBody));
    helper.getHttpSender().sendAndReceive(msg);
    print("\nReceived response status code for authentication request: " + str(msg.getResponseHeader()));
    print("\nResponseBody: " + str(msg.getResponseBody()));
    print("------------------------------------");
    return msg;
 
# function to set the token in Authorization header and in cookie in request
def setAccessTokenInHttpMessage(accessToken, msg):
    print "setting token in request"
    msg.getRequestHeader().setHeader("Authorization", "Bearer " + accessToken);
    cookie = HtmlParameter(HtmlParameter.Type.cookie, "token", accessToken);
    cookies = msg.getRequestHeader().getCookieParams();
    cookies.add(cookie);
    msg.getRequestHeader().setCookieParams(cookies);
 
#we have to make this function synchronized as we do not want to have duplicate concurrent attempts to login
@make_synchronized
def refreshAccessToken(helper):
    print "refreshing access token and checking if it has already been refreshed"
    accessToken = GlobalVariables.getGlobalVar("accessToken");    
    if accessToken is not None and isTokenValid(accessToken, helper) == True:
        print "access token already refreshed, no need to relogin"
        return accessToken;
 
    GlobalVariables.setGlobalVar("accessToken", None);
    accessToken = doLogin(helper);
    GlobalVariables.setGlobalVar("accessToken", str(accessToken)); 
    print "access token refreshed"
    return accessToken;

For those who want to know how we arrived at the script, let’s go through the steps.
Assuming Juice Shop is already running, we launch the Firefox browser from Zap. Go to the juice shop url. 

Go to the juice shop url. At the top right of the website, you will find Account-> Login. Click on the Login button.

Click on the link pointed to by the red arrow to create an account. In this example, we are using test@email.com and password for both email and password.

Once that is done, we log out and relogin again to get the login url. Now we go back to zap and see that we have the login url in zap. Take note also the /whoami link where we send in the token and get back the details of the user. if we send in an invalid token, we get back a json response with empty user details. this is how we know that the token is invalid.

Let’s take a look at how that looks like in terms of the request and response. So we send in a post http request with the username and password to the login url. and the response we get back is a token in json format.

We now can use the httpsender script from above. We add in a new httpsender script under the scripts tab and copy paste the code from above into it. 

Remember to enable the script or it won’t run when you start the ajax spider. Now, lets start the ajax spider.

You can see from the script console that the script is called and you are getting the access token. Go to any of the links found in the ajax spider and you can see that the token is added as an authorization header and also as a cookie in each and every request. Take note also that authentication statistics (https://www.zaproxy.org/docs/desktop/addons/authentication-statistics/) will not be updated because we are not using the the normal authentication method. The only way to tell if the authentication works during spidering is when the spider reaches the restricted section of the Juice shop, it is returning a valid response, ,just like how you are exploring the juice shop through the browser.

Here is also a tip for you when you are trying to debug the httpsender script but the global token has already been set. You can clear it off by creating a standalone script like below to clear off the global token before running your ajax spider to execute the httpsender script.

Leave a Comment

Your email address will not be published. Required fields are marked *