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 = apiKeyapiKey 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.authTokencallonce 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_tokenform 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.accessTokenTesting 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 ofkarate-config.js.callSingle()in Background runs once per feature file (because Background itself runs before every scenario, butcallSingleis cached at the feature level). To run truly once per suite,callSingle()must be inkarate-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 withjava.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 401on a protected endpoint with no token.
🎯 Practice task
Wire up authentication at three levels of scope. 40–50 minutes.
- In
karate-config.js, add a hard-coded fakeauthToken(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. - Create
common/login.featurethat simulates login by calling any JSONPlaceholder endpoint and returning a fakeauthTokenviadef. Usecalloncein Background to get the token. Run three scenarios and confirm the login request appears only once in the HTML report. - Add a scenario that clears the auth header with
* configure headers = {}and makes a GET request. Assertstatus 200(JSONPlaceholder ignores auth, so 200 is expected). The point is to confirm the header-clearing pattern works. - Create
common/basic-auth.jswith the Base64 encoding function from this lesson. Load it in a feature file and set theAuthorizationheader with the result. Print the header value to confirm it starts withBasic. - Write
common/oauth-login.featurewith theform fieldpattern. Even without a real OAuth server, write the feature structure and add* print 'form fields set'to confirm the file loads correctly. - Stretch: in
karate-config.js, implementkarate.callSingle()pointing atcommon/login.feature. Usekarate.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 fromkarate-config.jsworks.
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.