Create Authentication Challenge
Create an origin-bound authentication challenge. This initiates the biometric authentication flow by sending a push notification to the user's mobile device.
Phishing Resistance
The origin parameter must match a previously registered origin. Requests with unregistered origins are rejected with 403 UNREGISTERED_ORIGIN. This prevents real-time phishing attacks.
Endpoint
POST /api/v1/auth/challenge
Request Body
{
"user_email": "user@example.com",
"origin": "https://app.example.com"
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
user_email | string | Yes | Email of the user to authenticate |
origin | string | Yes | Origin URL where authentication was initiated |
Response
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"expires_in": 120,
"origin": {
"display_name": "Example Application",
"url": "https://app.example.com"
},
"challenge_hmac": "a1b2c3d4e5f6..."
}
Response Fields
| Field | Type | Description |
|---|---|---|
challenge_id | string | UUID to poll for status |
expires_in | integer | Seconds until challenge expires (default: 120) |
origin.display_name | string | Name shown on mobile device |
origin.url | string | Origin URL |
challenge_hmac | string | HMAC of challenge for verification |
Errors
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Missing user_email or origin |
UNREGISTERED_ORIGIN | 403 | Origin not registered (phishing protection) |
PUSH_FAILED | 500 | Failed to reach user's device |
SERVER_BUSY | 503 | Challenge pool exhausted |
Examples
cURL
#!/bin/bash
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
API_BASE="https://ztxbas01.corezt.com"
USER_EMAIL="user@example.com"
ORIGIN="https://app.example.com"
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 32)
BODY="{\"user_email\":\"${USER_EMAIL}\",\"origin\":\"${ORIGIN}\"}"
SIGN_DATA="POST|/api/v1/auth/challenge|${TIMESTAMP}|${NONCE}|${BODY}"
SIGNATURE=$(echo -n "$SIGN_DATA" | openssl dgst -sha256 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST "${API_BASE}/api/v1/auth/challenge" \
-H "Content-Type: application/json" \
-H "X-Client-ID: ${CLIENT_ID}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: ${SIGNATURE}" \
-d "$BODY"
Python
import hmac
import hashlib
import time
import secrets
import requests
import json
class ZTXClient:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://ztxbas01.corezt.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
def _sign_request(self, method: str, endpoint: str, body: str) -> dict:
timestamp = str(int(time.time()))
nonce = secrets.token_hex(32)
sign_data = f"{method}|{endpoint}|{timestamp}|{nonce}|{body}"
signature = hmac.new(
self.client_secret.encode(),
sign_data.encode(),
hashlib.sha256
).hexdigest()
return {
"X-Client-ID": self.client_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json"
}
def create_challenge(self, user_email: str, origin: str) -> dict:
"""
Create an authentication challenge.
Args:
user_email: Email of the user to authenticate
origin: Origin URL (must be pre-registered)
Returns:
Challenge details including challenge_id for polling
Raises:
requests.HTTPError: If origin is not registered (403) or other error
"""
endpoint = "/api/v1/auth/challenge"
body = json.dumps({"user_email": user_email, "origin": origin})
headers = self._sign_request("POST", endpoint, body)
response = requests.post(f"{self.base_url}{endpoint}", headers=headers, data=body)
if response.status_code == 403:
raise ValueError(f"Origin not registered: {origin}")
response.raise_for_status()
return response.json()
# Usage
client = ZTXClient("your-client-id", "your-client-secret")
try:
challenge = client.create_challenge(
user_email="user@example.com",
origin="https://app.example.com"
)
print(f"Challenge ID: {challenge['challenge_id']}")
print(f"Expires in: {challenge['expires_in']} seconds")
print(f"Origin: {challenge['origin']['display_name']}")
except ValueError as e:
print(f"Phishing protection triggered: {e}")
Node.js
const crypto = require('crypto');
const https = require('https');
class ZTXClient {
constructor(clientId, clientSecret, baseUrl = 'https://ztxbas01.corezt.com') {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = new URL(baseUrl);
}
_signRequest(method, endpoint, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(32).toString('hex');
const signData = `${method}|${endpoint}|${timestamp}|${nonce}|${body}`;
const signature = crypto
.createHmac('sha256', this.clientSecret)
.update(signData)
.digest('hex');
return {
'X-Client-ID': this.clientId,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'Content-Type': 'application/json'
};
}
async createChallenge(userEmail, origin) {
const endpoint = '/api/v1/auth/challenge';
const body = JSON.stringify({ user_email: userEmail, origin });
const headers = this._signRequest('POST', endpoint, body);
return new Promise((resolve, reject) => {
const options = {
hostname: this.baseUrl.hostname,
port: this.baseUrl.port || 443,
path: endpoint,
method: 'POST',
headers
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const result = JSON.parse(data);
if (res.statusCode === 403) {
reject(new Error(`Origin not registered: ${origin}`));
return;
}
if (res.statusCode !== 200) {
reject(new Error(result.message || 'Challenge creation failed'));
return;
}
resolve(result);
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
}
// Usage
const client = new ZTXClient('your-client-id', 'your-client-secret');
async function authenticate(userEmail, origin) {
try {
const challenge = await client.createChallenge(userEmail, origin);
console.log(`Challenge ID: ${challenge.challenge_id}`);
console.log(`Expires in: ${challenge.expires_in} seconds`);
console.log(`Origin: ${challenge.origin.display_name}`);
return challenge;
} catch (error) {
if (error.message.includes('not registered')) {
console.error('Phishing protection triggered!');
}
throw error;
}
}
authenticate('user@example.com', 'https://app.example.com');
C# (.NET)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class ZTXClient
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly HttpClient _httpClient;
public ZTXClient(string clientId, string clientSecret, string baseUrl = "https://ztxbas01.corezt.com")
{
_clientId = clientId;
_clientSecret = clientSecret;
_httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
}
private Dictionary<string, string> SignRequest(string method, string endpoint, string body)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLower();
var signData = $"{method}|{endpoint}|{timestamp}|{nonce}|{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_clientSecret));
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(signData))).ToLower();
return new Dictionary<string, string>
{
["X-Client-ID"] = _clientId,
["X-Timestamp"] = timestamp,
["X-Nonce"] = nonce,
["X-Signature"] = signature
};
}
public async Task<ChallengeResponse> CreateChallengeAsync(string userEmail, string origin)
{
var endpoint = "/api/v1/auth/challenge";
var body = JsonSerializer.Serialize(new { user_email = userEmail, origin });
var headers = SignRequest("POST", endpoint, body);
var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
var response = await _httpClient.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new InvalidOperationException($"Origin not registered: {origin}");
}
response.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<ChallengeResponse>(json, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
}
}
public class ChallengeResponse
{
public string ChallengeId { get; set; }
public int ExpiresIn { get; set; }
public OriginInfo Origin { get; set; }
public string ChallengeHmac { get; set; }
}
public class OriginInfo
{
public string DisplayName { get; set; }
public string Url { get; set; }
}
// Usage
var client = new ZTXClient("your-client-id", "your-client-secret");
try
{
var challenge = await client.CreateChallengeAsync(
"user@example.com",
"https://app.example.com"
);
Console.WriteLine($"Challenge ID: {challenge.ChallengeId}");
Console.WriteLine($"Expires in: {challenge.ExpiresIn} seconds");
Console.WriteLine($"Origin: {challenge.Origin.DisplayName}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Phishing protection triggered: {ex.Message}");
}
Best Practices
- Always use the browser's origin: Use
window.location.originto get the correct value - Handle 403 gracefully: Show appropriate error if origin is not registered
- Show waiting UI: Display a spinner while polling for user response
- Implement timeout: Stop polling after
expires_inseconds - Retry logic: Implement exponential backoff for polling