-
Notifications
You must be signed in to change notification settings - Fork 27
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
 | ||
|
||
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. | ||
|
||
### 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). | ||
|
||
 | ||
|
||
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=/;'); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.