Forms with Username and Passwords

Overview

A common way to authenticate to a web application is by POSTing a username and password which can be verified by your server. Upon verification the server returns a cookie or token to the requesting client.

When giving HawkScan Username and Password credentials to login to your app, HawkScan needs to know if the server will return a cookie or a token to properly maintain the session.

The authentication section of the stackhawk.yml will have 4 parts:

  1. Logged in/out indicators
    • How HawkScan checks it is logged in throughout the scan.
  2. Auth(N)
    • Your form type and login credentials.
  3. Auth(Z)
    • How you maintain the session. Either a Cookie or Token.
  4. Test Path
    • How HawkScan sees if it successfully logged in

Make sure your file has all 4 of these parts filled out.

YAML by Form Type

HTTP Parameter Form:

stackhawk.yml

app:
  applicationId: kkAAAKAW-kAWW-kkAA-WWwW-kAAkkAAAAwWW
  env: Test
  host: http://localhost:9000
  antiCsrfParam: csrfToken
  authentication:
    # Paths that HawkScan checks to see if it is still logged in during the scan
    loggedInIndicator: "HTTP.*2[0-9][0-9]\\s*O[kK](\\s*)|HTTP.*3[0-9][0-9].*" # Change me
    loggedOutIndicator: "HTTP.*4[0-9][0-9](\\s*)Unauthorized.*" # Change me
    # Auth(N) HTTP Form
    usernamePassword:
      type: FORM
      loginPagePath: /login # Your login form path
      loginPath: /login # Your login form path
      usernameField: email # Field name for the account username/email
      passwordField: password # Field name for the password
      scanUsername: ${SCAN_USERNAME} # Inject variable at runtime or place your username here
      scanPassword: ${SCAN_PASSWORD} # Inject variable at runtime or place your password here  
      otherParams:
        - name: rememberMe
          val: "true"
    # (REQUIRED) Add your Auth(Z) here. Either Cookie or Token
    #
    #A path that can only be seen when successfully logged in. HawkScan will check this path to see if log in was successfull
    testPath:
      path: /mysettings # Change me
      success: ".*2[0-9]{2}.*"
      requestMethod: GET

Form with API Call / JSON Payload

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: http://localhost:3000
  authentication:
    # Paths that HawkScan checks to see if it is still logged in during the scan
    loggedInIndicator: "HTTP.*2[0-9][0-9]\\s*O[kK](\\s*)|HTTP.*3[0-9][0-9].*" # Change me
    loggedOutIndicator: "HTTP.*4[0-9][0-9](\\s*)Unauthorized.*" # Change me
    # Auth(N) Form with API Call / JSON Payload
    usernamePassword:
      type: JSON
      loginPath: /login # Your login form path
      usernameField: email # Field name for the account username/email
      passwordField: password # Field name for the password
      scanUsername: ${SCAN_USERNAME} # Inject variable at runtime or place your username here
      scanPassword: ${SCAN_PASSWORD} # Inject variable at runtime or place your password here
      otherParams:
        - name: rememberMe
          val: "true"
    # Auth(Z) Token
    tokenAuthorization:
      type: HEADER
      value: X-APIKey
    # Additional configuration to tell HawkScan how to get the token from the response body.
    tokenExtraction:
      type: TOKEN_PATH
      value: "authentication.token"
    # A path that can only be seen when successfully logged in. HawkScan will check this path to see if log in was successfull
    testPath:
      path: /mysettings #Change me
      success: ".*2[0-9]{2}.*"
      requestMethod: GET

YAML Sections in Detail

Giving HawkScan Form Credentials

There are two ways to give HawkScan the Username and Password credentials to login to your application.

  1. HawkScan best practices is using environment variable runtime overrides. This is the most secure way to keep valid credentials to your application secret. These can be pulled in from your run command or a stored secret in your pipeline.

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: http://localhost:3000
  authentication:
    usernamePassword:
      scanUsername: ${SCAN_USERNAME}
      scanPassword: ${SCAN_PASSWORD}
  1. The simpler, but less secure way, to do this is to directly put the Username and Password into the YAML file. Add the desired Username and Password to the corresponding .scanUsername and .scanPassword fields.

Adding otherParams

Sometimes login forms have more than just a Username and Password required to login. A common example of this is “Remember Me” checkboxes.

If your form has more fields that need to be filled out, you can do so under otherParams. Give the name of the field and the required val.

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: http://localhost:3000
  authentication:
    usernamePassword:
      otherParams:
        - name: rememberMe
          val: "true"
        - name: extraParam
          val: "myString"

Maintaining the Session

Once HawkScan has logged in with the username and password form, add the following YAML snippet depending on if your application is using cookies or tokens.

Cookies:

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: ${APP_HOST:http://localhost:3000}
  authentication:
    cookieAuthorization:
      cookieNames:
        - sessionid

Tokens:

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: ${APP_HOST:http://localhost:3000}
  authentication:
    tokenAuthorization:
      type: HEADER
      value: X-APIKey

Custom Script:

Custom authentication and session management scripts can be used to handle complex authentication and authorization scenarios. If a preconfigured authentication and/or authorization style doesn’t meet your needs you can replace either with a custom script. Visit our GitHub repo to get started.

Test Paths and Logged In/Out Indicators

No matter what type of Authorization/Authentication your app is using, HawkScan requires .testPath, .loggedInIndicator and .loggedOutIndicator.

Test Path:

A testPath configuration needs to be provided to verify HawkScan authenticated its session correctly before scanning the application. The testPath configuration also provides requestMethod and requestBody options to support alternate HTTP request verbs, such as POST or PUT. The default action is GET.

Your testPath should be a path that HawkScan can only access when successfully logged in. This could be a path like “/dashboard” or “/accountdetails”

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: ${APP_HOST:http://localhost:3000}
  authentication:
    testPath:
      path: /mysettings
      success: ".*2[0-9]{2}.*"
      requestMethod: GET

Logged In/Out Indicators

Throughout the scan, HawkScan will check to see if it is still logged in by the .loggedInIndicator and .loggedOutIndicator. These are regex strings to match against http responses from requests in the web application.

A .loggedInIndicator could be a “Log Out” or ”Sign Out” button a user would see if logged in. An example of a .loggedOutIndicator would be a “Log In” button on the sign in page. These can also leverage http status codes from the response.

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: ${APP_HOST:http://localhost:3000}
  authentication:
    loggedInIndicator: "HTTP.*2[0-9][0-9]\\s*O[kK](\\s*)|HTTP.*3[0-9][0-9].*"
    loggedOutIndicator: "HTTP.*4[0-9][0-9](\\s*)Unauthorized.*"

This is the standard behavior for cookie-based authorization schemes. The cookie is used to track your session on the server with the expectation that subsequent requests send the cookie back via the Set-Cookie response header. This allows the server to track requests and maintain the session.

The POST request for sending the username and password expects a payload type of application/x-www-form-urlencoded. The cookie verifying the user’s session will be named sessionid.

When a request with an invalid, or non-present cookie is sent the server will respond by redirecting the user to the /login page.

Create or use an existing stackhawk.yml to add support for scanning as an authenticated user.

Let’s say your web application has a path structure like the following, with each request returning standard html content type text/html.

GET|POST  /login
GET       /home
GET       /comments
GET|POST  /mysettings <- protected route
POST      /comments <- protected route
GET|POST  /privatemessage <- protected routes

Detailed Configuration

You would want a stackhawk.yml configuration like this.

stackhawk.yml

app:
  applicationId: kkAAAKAW-kAWW-kkAA-WWwW-kAAkkAAAAwWW
  env: Test
  host: http://localhost:9000
  antiCsrfParam: csrfToken
  authentication:
    loggedInIndicator: "\\QLog out\\E"
    loggedOutIndicator: "\\QLog in\\E"
    usernamePassword:
      type: FORM
      loginPagePath: /login
      loginPath: /login
      usernameField: email
      passwordField: password
      scanUsername: ${SCAN_USERNAME}
      scanPassword: ${SCAN_PASSWORD}      
      otherParams:
        - name: rememberMe
          val: "true"
    cookieAuthorization:
      cookieNames:
        - sessionid
    testPath:
      path: /mysettings
      fail: ".*302.*Location:.*/login.*"

The authentication request/response would look like the following. The response payloads have been truncated for brevity denoted by ...

Auth Request Step #1

GET /login HTTP/1.1
Host: localhost:9000
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

Auth Response Step #1

HTTP/1.1 200 OK
Date: Thu, 24 Oct 2019 21:31:50 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 6058

...
<form action="/login" method="POST">
    <input type="hidden" name="csrfToken"
           value="c9daeffb9679563ec924ac110b44677b5e3710af-1571951867944-5df593a128612bdbb7c76af9"/>
    <div class="form-group  " id="email_field">

        <label class="control-label sr-only" for="email">Email</label>
        <input type="email" id="email" name="email" value="" class="form-control form-control input-lg"
               placeholder="Email">
    </div>
    <div class="form-group  " id="password_field">
        <label class="control-label sr-only" for="password">Password</label>
        <input type="password" id="password" name="password" value="" required="true"
               class="form-control form-control input-lg" placeholder="Password">
    </div>
...

The step #1 request is a GET request to the app.authentication.loginPath to return the login form containing the hidden input field csrfToken. The csrfToken is often required for form-based authentication as a means of ensuring the form POST originated from the intended web form.

Auth Request Step #2

POST /login HTTP/1.1
Host: localhost:9000
Connection: keep-alive
Content-Length: 149
Origin: http://localhost:9000
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

csrfToken=c9daeffb9679563ec924ac110b44677b5e3710af-1571951867944-5df593a128612bdbb7c76af9&email=user%40example.com&password=s00p3rS33kretP@55w0rd&rememberMe=true

Auth Response Step #2

HTTP/1.1 303 See Other
Location: /
Set-Cookie: sessionid=1-36b182ae988ccc397683f134a3a508e0a246ee2c-1-ZUdGVa7DthmVGlbnOs+ckrnmOUdoJb5vMHxIlwPeriJflTOPqxjc9SnQ0X5AhY+VY9Mtow4YPDwp5RXCiau96MKKCCxRZuiovGU+kDKtrz+9NdvSmCNZiGRtOLaWXliOvBefeiet2EkaNpH3EeKVa2rLCM5aYctB75tdDo8X6xHyh4bc1CxJy9IXDABZ1eGXhmxAfBFjowRHdPcbCi68MMKHgQZOG1xvHfIR9bCaQw66sdqrjP0jb6mXhrdYz7F1wxfSUVMUQwIa8bveXxvMmh8kqc76XBRZ+wZwSb7CTO4iIk6DXItpSzqiumRv6gdxyGILUe5hoOS+CoSsDZPI5Q/qwaKce7XhXQy9YWYZ1wwf7cbfn9W5W83jTwqZKCf34W5OxDwgFO/8qvsBBCUT+sEpW+5j9hz0Y25raTNxCi450cYjEu+w0gCVnqXeA2vfRFqsKILaW58mxJuuux51NpXuVPnPgbESkKMgfonuce2TK6YVurHF5z+Rduwe/VfaEx48sT+TuPOg6gIV46j7fKrdJXH4+I3jHX9upMi/k9FS31I8IYFjdx6VkXFsH8hwz2AN2l0xwMqtFxwUIRNtqOpTwODb1F1NMYw/2Ku4VTR0rVTRj/mAeWt4OsRJHROs7JKKFYPVYD4swerTuMU3l+QbAQUJrJDZRKW5qjbrXVQ+DNCcsPlkGTPXhU1vA5eXJVdES0AeN+Zp1lE4Vzl4reXxgEE26wxWQ+CyStnkBt8=; Max-Age=2592000; Expires=Sat, 23 Nov 2019 21:30:19 GMT; SameSite=Lax; Path=/; HTTPOnly
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Date: Thu, 24 Oct 2019 21:30:19 GMT
Content-Length: 0

The step two request is a POST as a form-urlencoded payload with the fields specified in your stackhawk.yml as a well as the csrfToken value gathered from step one. The returning payload also contains the Set-Cookie header with a cookie value that will be sent on subsequent authenticated requests. The cookie name and csrf token name are specified in the stackhawk.yml as app.cookAuthorization.cookieNames and app.antiCsrfParam respectively.

Verifying authentication

The testPath configuration will tell HawkScan how to verify if authentication has been configured correctly. A GET request will be sent to testPath whose response will be verified against the testPath.fail or testPath.success criteria. In this scenario you can define fail criteria as the regex .*302.*Location.*/login.* which will consider a Location redirect to the /login page an indication of failed authentication. If the regex does not match the response header from testPath.path authentication will be considered a success. See more detail on testPath in the Configuration docs.

Authorized Request to /mysettings

GET /mysettings HTTP/1.1
Host: localhost:9000
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: sessionid=1-36b182ae988ccc397683f134a3a508e0a246ee2c-1-ZUdGVa7DthmVGlbnOs+ckrnmOUdoJb5vMHxIlwPeriJflTOPqxjc9SnQ0X5AhY+VY9Mtow4YPDwp5RXCiau96MKKCCxRZuiovGU+kDKtrz+9NdvSmCNZiGRtOLaWXliOvBefeiet2EkaNpH3EeKVa2rLCM5aYctB75tdDo8X6xHyh4bc1CxJy9IXDABZ1eGXhmxAfBFjowRHdPcbCi68MMKHgQZOG1xvHfIR9bCaQw66sdqrjP0jb6mXhrdYz7F1wxfSUVMUQwIa8bveXxvMmh8kqc76XBRZ+wZwSb7CTO4iIk6DXItpSzqiumRv6gdxyGILUe5hoOS+CoSsDZPI5Q/qwaKce7XhXQy9YWYZ1wwf7cbfn9W5W83jTwqZKCf34W5OxDwgFO/8qvsBBCUT+sEpW+5j9hz0Y25raTNxCi450cYjEu+w0gCVnqXeA2vfRFqsKILaW58mxJuuux51NpXuVPnPgbESkKMgfonuce2TK6YVurHF5z+Rduwe/VfaEx48sT+TuPOg6gIV46j7fKrdJXH4+I3jHX9upMi/k9FS31I8IYFjdx6VkXFsH8hwz2AN2l0xwMqtFxwUIRNtqOpTwODb1F1NMYw/2Ku4VTR0rVTRj/mAeWt4OsRJHROs7JKKFYPVYD4swerTuMU3l+QbAQUJrJDZRKW5qjbrXVQ+DNCcsPlkGTPXhU1vA5eXJVdES0AeN+Zp1lE4Vzl4reXxgEE26wxWQ+CyStnkBt8=

Requests to protected routes like /mysettings, /comments and /privatemessages will then respect the value supplied by the sessionid cookie allowing for access to protected routes during a scan.

Changes to the sessionid cookie via the Set-Cookie header will be updated automatically and used as the new cookie value across multiple authenticate scan requests.

If the response payload contains text matching the regex supplied in the app.authentication.loggedOutIndicator, the scanner will presume its session cookie has become invalid and will perform the authentication steps described above to ensure it can continue making authenticated requests with a new authenticated session.

Example Form with API Call / JSON Payload with Token Authorization

Many modern web application backends are APIs that serve data to more than just html based web browsers. These apps do not rely on web forms for authentication. This approach is common for single page applications that use modern javascript frameworks like Angular, React, Vue.js, and others.

A common approach for authentication in this scenario is to create an API route that accepts a user’s credentials via a POST request of JSON data with the request returning an Authorization token as part of the JSON response payload. The API then expects the token be sent on all subsequent requests to protected API routes as an Authorization header. This method of authorization is commonly referred to as bearer token authorization.

HawkScan supports this scenario by adding usernamePassword, tokenExtraction, and tokenAuthorization configurations to app.authentication.

Configuring usernamePassword for this scenario is similar to the first scenario, but instead setting usernamePassword.type=JSON will cause the credential and otherParams to be POSTed as application/json instead of application/x-www-form-urlencoded. Successful authentication will return a JSON payload containing the bearer token that will need to be passed with every request to an authenticated route.

Let’s say your web application API has a path structure like the following, with each route expecting application/json as request and response content types.

Note: In this scenario the tokenExtraction configuration will be used to describe how to acquire the token. The tokenAuthorization configuration will be used to describe how to pass the extracted token on all scan requests.

POST      /login
GET       /home
GET       /comments
GET|POST  /mysettings <- protected route
POST      /comments <- protected route
GET|POST  /privatemessage <- protected routes

Detailed Configuration

You would want a stackhawk.yml configuration like this.

stackhawk.yml

app:
  applicationId: xxXXXXXX-xXXX-xxXX-XXxX-xXXxxXXXXxXX
  env: Test
  host: http://localhost:3000
  authentication:
    loggedInIndicator: "HTTP.*2[0-9][0-9]\\s*O[kK](\\s*)|HTTP.*3[0-9][0-9].*"
    loggedOutIndicator: "HTTP.*4[0-9][0-9](\\s*)Unauthorized.*"
    usernamePassword:
      type: JSON
      loginPath: /login
      usernameField: email
      passwordField: password
      scanUsername: ${SCAN_USERNAME}
      scanPassword: ${SCAN_PASSWORD}
      otherParams:
        - name: rememberMe
          val: "true"
    tokenExtraction:
      type: TOKEN_PATH
      value: "authentication.token"
    tokenAuthorization:
      type: HEADER
      value: Authorization
      tokenType: Bearer
    testPath:
      path: /mysettings
      success: ".*200.*"

The authentication request/response would then look something like.

Auth Request

POST /login HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Content-Length: 83
Accept: application/json, text/plain, */*

{"email":"user@example.com","password":"s00p3rS33kretP@55w0rd","rememberMe":"true"}

Auth Response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 477
Date: Thu, 24 Oct 2019 17:53:50 GMT
Connection: keep-alive

{
  "authentication": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJ1aWQiOjExMSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoidXNlciIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTEwLTI0IDE3OjM5OjAyLjI3NCArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTEwLTI0IDE3OjM5OjAyLjI3NCArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NzE5Mzg3OTQsImV4cCI6MTU3MTk1Njc5NH0.Y7y2YOKNn-DCarSp_n8zZkFuXJbL0QoCmCKvmE7bCZg",
    "uid": 111
  }
}

Authorized Request to /mysettings

GET /mysettings HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Accept: application/json, text/plain, */*
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJ1aWQiOjExMSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlIjoidXNlciIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTEwLTI0IDE3OjM5OjAyLjI3NCArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTEwLTI0IDE3OjM5OjAyLjI3NCArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NzE5Mzg3OTQsImV4cCI6MTU3MTk1Njc5NH0.Y7y2YOKNn-DCarSp_n8zZkFuXJbL0QoCmCKvmE7bCZg

Requests to protected routes like /mysettings, /comments and /privatemessages will have the Authorization header added to each request as specified in the tokenAuthorization configuration. With the type header describing the token being as a request header, the value being the name of the header Authorization, and tokenType: Bearer prepended to the token.