Authentication — Headers, Tokens, OAuth

9 min read

The API Testing Masterclass lesson on authentication covered the concepts: API keys, Bearer tokens, Basic auth, and OAuth 2.0 flows. This lesson is the Karate implementation of all four. By the end you'll have a karate-config.js that logs in once per test run, sets a global auth header, and makes every feature file in your project authenticated by default — without touching the individual feature files.

API key in a header

The simplest form — a static key added to every request:

Background:
  * url baseUrl
  * header X-API-Key = 'abc123def456'

For keys that come from the environment rather than being hardcoded:

// karate-config.js
function fn() {
    return {
        baseUrl: 'https://api.staging.myapp.com',
        apiKey: java.lang.System.getenv('API_KEY') || 'dev-key-for-local'
    };
}
Background:
  * url baseUrl
  * header X-API-Key = apiKey

apiKey comes from karate-config.js. In CI, the API_KEY environment variable is set as a secret. Locally, it falls back to a development key. The feature file never contains the actual secret.

Bearer tokens — login once, use everywhere

The standard pattern from the previous chapter scaled to the whole project:

# common/login.feature
Feature: Login Helper
Scenario:
  Given url baseUrl
  And path 'auth/login'
  And request { email: '#(email)', password: '#(password)' }
  When method post
  Then status 200
  * def authToken = response.token
# Any feature file's Background
Background:
  * def loginResult = callonce read('classpath:common/login.feature') { email: 'admin@test.com', password: 'AdminPass' }
  * header Authorization = 'Bearer ' + loginResult.authToken

callonce means the login runs exactly once per feature file — one HTTP call, shared token. Every scenario in the file inherits the Authorization header.

Global auth via karate-config.js

For projects where every feature file needs the same token, set it globally in karate-config.js using karate.callSingle() and karate.configure():

function fn() {
    var config = {
        baseUrl: 'https://api.staging.myapp.com'
    };
 
    // Login once per test run — not per feature, not per scenario
    var result = karate.callSingle('classpath:common/login.feature', {
        email: 'admin@test.com',
        password: java.lang.System.getenv('ADMIN_PASSWORD') || 'AdminPass'
    });
 
    karate.configure('headers', {
        Authorization: 'Bearer ' + result.authToken
    });
 
    return config;
}

karate.callSingle() runs the called feature exactly once for the entire test suite — not once per feature file, not once per scenario. The result is cached. karate.configure('headers', {...}) sets a persistent header map that Karate appends to every request across the whole run. No Background block needed in any feature file — they're all authenticated automatically.

Basic auth

Basic auth encodes username:password as Base64. Call a JavaScript helper that does the encoding:

// common/basic-auth.js
function fn(creds) {
    var Base64 = Java.type('java.util.Base64');
    var token  = creds.username + ':' + creds.password;
    var encoded = Base64.getEncoder().encodeToString(
        new java.lang.String(token).getBytes('UTF-8'));
    return 'Basic ' + encoded;
}
* header Authorization = call read('classpath:common/basic-auth.js') { username: 'alice', password: 'pass123' }

The function receives the argument object, encodes the credentials, and returns the header value as a string. The call keyword without read() on the left assigns the return value directly.

OAuth 2.0 client credentials flow

OAuth requires a separate token request before the API call. Write a dedicated feature for it:

# common/oauth-login.feature
Feature: OAuth Client Credentials
Scenario:
  Given url 'https://auth.example.com/oauth/token'
  And form field grant_type    = 'client_credentials'
  And form field client_id     = '#(clientId)'
  And form field client_secret = '#(clientSecret)'
  When method post
  Then status 200
  * def accessToken = response.access_token

form field sends application/x-www-form-urlencoded — the format OAuth token endpoints expect. Karate sets the correct Content-Type header automatically when you use form field instead of request.

Background:
  * def oauthResult = callonce read('classpath:common/oauth-login.feature') {
      clientId: 'my-app',
      clientSecret: java.lang.System.getenv('CLIENT_SECRET')
    }
  * header Authorization = 'Bearer ' + oauthResult.accessToken

Testing auth failure cases

Testing that your API correctly rejects unauthenticated requests is as important as testing that it accepts authenticated ones:

Scenario: Request without token returns 401
  * configure headers = {}
  Given url baseUrl
  And path 'users'
  When method get
  Then status 401
  And match response.error == '#string'
 
Scenario: Request with expired token returns 401
  * header Authorization = 'Bearer expired-token-value'
  Given url baseUrl
  And path 'users'
  When method get
  Then status 401

* configure headers = {} clears any globally-set headers for this scenario — useful when global auth is set in karate-config.js and you need one scenario to run unauthenticated.

The global auth flow

Step 1 of 5

mvn test starts

Karate begins the test run. karate-config.js executes once — before any feature file runs.

⚠️ Common mistakes

  • Using callSingle() in Background instead of karate-config.js. callSingle() in Background runs once per feature file (because Background itself runs before every scenario, but callSingle is cached at the feature level). To run truly once per suite, callSingle() must be in karate-config.js. Misplacing it means repeated login calls, token expiry surprises, and wasted time.
  • Hardcoding credentials in feature files or karate-config.js. Credentials committed to a repository are a security incident waiting to happen. Always read passwords and secrets from environment variables with java.lang.System.getenv('KEY') and provide a safe fallback only for local development. Never commit the actual secret, even for staging environments.
  • Forgetting to test the unauthenticated path. A suite that only tests authenticated requests gives you no coverage for 401 cases. A real regression: auth middleware is accidentally removed and all requests return 200 — your test suite stays green. Always include at least one scenario that asserts status 401 on a protected endpoint with no token.

🎯 Practice task

Wire up authentication at three levels of scope. 40–50 minutes.

  1. In karate-config.js, add a hard-coded fake authToken (e.g., 'local-dev-token') to the config object. In a feature file's Background, use * header Authorization = 'Bearer ' + authToken. Run a GET scenario and confirm the header appears in the HTML report's request section.
  2. Create common/login.feature that simulates login by calling any JSONPlaceholder endpoint and returning a fake authToken via def. Use callonce in Background to get the token. Run three scenarios and confirm the login request appears only once in the HTML report.
  3. Add a scenario that clears the auth header with * configure headers = {} and makes a GET request. Assert status 200 (JSONPlaceholder ignores auth, so 200 is expected). The point is to confirm the header-clearing pattern works.
  4. Create common/basic-auth.js with the Base64 encoding function from this lesson. Load it in a feature file and set the Authorization header with the result. Print the header value to confirm it starts with Basic .
  5. Write common/oauth-login.feature with the form field pattern. Even without a real OAuth server, write the feature structure and add * print 'form fields set' to confirm the file loads correctly.
  6. Stretch: in karate-config.js, implement karate.callSingle() pointing at common/login.feature. Use karate.configure('headers', { Authorization: 'Bearer ' + result.authToken }). Remove the Background auth block from your feature file and confirm requests still carry the header — proving global auth from karate-config.js works.

Next lesson: retry and polling patterns — retry until for waiting on async processes, configuring retry count and interval, and the use cases where polling belongs.

// tip to track lessons you complete and pick up where you left off across devices.