;
}
```
| Category | Meaning | Recovery |
|----------|---------|----------|
| `retryable` | Transient error (network, rate limit) | Retry the same action |
| `user_action` | User error (wrong code, invalid input) | Show error, let user retry |
| `fatal` | Unrecoverable (session expired) | Call `reset()` and start over |
### Step Types
When `state.status === 'step'`, render UI based on `state.step.type`:
#### `select_method`
User chooses an authentication method.
```typescript
interface SelectMethodPrompt {
type: 'select_method';
methods: Array<{ name: string; label: string; }>;
}
```
**Action:** Call `selectMethod(method.name)`
#### `enter_code`
User enters an OTP code sent via email or SMS.
```typescript
interface EnterCodePrompt {
type: 'enter_code';
destination: string; // Masked email/phone
codeLength: number; // Expected code length
}
```
**Action:** Call `submitCode(code)`
#### `enter_totp`
User enters a code from their authenticator app.
```typescript
interface EnterTotpPrompt {
type: 'enter_totp';
}
```
**Action:** Call `submitTotp(code)`
#### `setup_totp`
User sets up TOTP for the first time.
```typescript
interface SetupTotpPrompt {
type: 'setup_totp';
secret: string; // Manual entry key
otpauthUrl: string; // QR code URL
}
```
**UI:** Show QR code from `otpauthUrl`, display `secret` for manual entry.
**Action:** Call `submitTotp(code)` after user enters code from authenticator app.
#### `setup_sms`
User enrolls their phone number.
```typescript
interface SetupSmsPrompt {
type: 'setup_sms';
}
```
**Action:** Call `submitPhone(phoneNumber)` (E.164 format, e.g. `+15551234567`)
#### `await_approval`
Waiting for external approval (QR scan, push notification).
```typescript
interface AwaitApprovalPrompt {
type: 'await_approval';
qrData?: string;
}
```
**UI:** Show QR code if `qrData` provided, otherwise show "Waiting for approval..."
**Action:** None — SDK polls automatically and transitions when approved.
#### `redirect`
Redirect to external OAuth provider.
```typescript
interface RedirectPrompt {
type: 'redirect';
url: string;
}
```
**Action:** `window.location.href = step.url`
### SDK Methods
| Method | When to Call |
|--------|--------------|
| `start(email, flowType?)` | Begin authentication. `flowType` optional: `'signup'` or `'account_manage'` |
| `selectMethod(methodId)` | After `select_method` step |
| `submitCode(code)` | After `enter_code` step |
| `submitTotp(code)` | After `enter_totp` or `setup_totp` step |
| `submitPhone(phone)` | After `setup_sms` step |
| `resend()` | Resend OTP code (valid after `enter_code` step) |
| `reset()` | Clear state, return to `idle` |
| `cancel()` | Cancel the current flow |
| `getState()` | Get current state |
| `onStateChange(callback)` | Subscribe to state changes. Returns unsubscribe function |
If `flowType` is omitted from `start()`, the SDK auto-detects new vs returning users.
### React Hooks
```tsx
import { HawcxProvider, useAuthState, useAuthActions, useAuthClient } from '@hawcx/react';
// Wrap your app
// In components:
const state = useAuthState();
const { start, selectMethod, submitCode, submitTotp, submitPhone, resend, reset, cancel } = useAuthActions();
const client = useAuthClient(); // underlying client instance
```
### MFA & Step-Up
Use `start(email, 'account_manage')` when a signed-in user performs a sensitive action (changing email, exporting data, billing). The flow is identical — same steps, same backend exchange.
### Device Revocation & Single-Logout (added in `@hawcx/core` / `@hawcx/react` 2.6.0)
The 2.6.0 release added device-revocation handling and single-logout. Upgrade
`@hawcx/core` **and** `@hawcx/react` to 2.6.0 together (React 2.6.0 imports
`getAuthNotice` from core; core 2.4.x doesn't have it).
- **`sessionToken`** — on the `completed` `AuthResult` (`{ authCode, expiresAt, sessionToken? }`), the hx_auth session JWT; use it as the bearer for the calls below.
- **`getAuthNotice(state): AuthNotice | null`** (from `@hawcx/core`) — the non-fatal, server-driven notice on the current step. `AuthNotice = { code: string; message: string }`. When an admin revokes a device, its next sign-in returns `403 unauthorized_device`; the SDK auto-recovers (clears the revoked credential, replays once skipping device-trust so the user re-verifies) and rides a notice on the verification step instead of throwing.
- **`client.logoutEverywhere(bearerToken): Promise<{ sessionsEnded: number }>`** — end all of the user's sessions across every device. Pass the `sessionToken`.
- **`client.resumeSession(session, identifier?)` / `client.getPkceVerifier()`** — for redirect flows: before redirecting persist `getSessionId()` + `getPkceVerifier()`, then adopt the flow on the fresh client with `resumeSession(...)`.
```typescript
// Vanilla core: detect a device-revocation notice + single-logout
const notice = getAuthNotice(client.getState());
if (notice?.code === 'unauthorized_device') { /* show notice.message; user re-verifies */ }
const result = client.getCompletion();
if (result?.sessionToken) {
const { sessionsEnded } = await client.logoutEverywhere(result.sessionToken);
}
```
React exposes the same via **`useAuthFlags()`** and **`useAuthActions()`**:
```tsx
const { notice, deviceRevoked } = useAuthFlags(); // deviceRevoked = notice.code === 'unauthorized_device'
const { logoutAllDevices } = useAuthActions(); // wraps client.logoutEverywhere(sessionToken)
```
---
## Web SDK — Complete React Example
```tsx
// src/App.tsx
import { HawcxProvider } from '@hawcx/react';
import { LoginFlow } from './LoginFlow';
export function App() {
return (
);
}
```
```tsx
// src/LoginFlow.tsx
import { useState, useEffect } from 'react';
import { useAuthState, useAuthActions } from '@hawcx/react';
export function LoginFlow() {
const state = useAuthState();
const { start, selectMethod, submitCode, submitTotp, submitPhone, reset } = useAuthActions();
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
// Handle completed state with useEffect (not in render)
useEffect(() => {
if (state.status === 'completed') {
fetch('/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
authCode: state.authCode,
codeVerifier: state.codeVerifier
})
}).then(() => window.location.href = '/');
}
}, [state.status]);
if (state.status === 'idle') {
return (
);
}
if (state.status === 'loading') return Loading...
;
if (state.status === 'error') {
return (
Error: {state.error.message}
);
}
if (state.status === 'completed') return Success! Redirecting...
;
if (state.status === 'step') {
const { step } = state;
if (step.type === 'select_method') {
return (
Choose method
{step.methods.map((m) => (
))}
);
}
if (step.type === 'enter_code') {
return (
);
}
if (step.type === 'enter_totp') {
return (
);
}
if (step.type === 'setup_totp') {
return (
Set up authenticator
Or enter manually: {step.secret}
);
}
if (step.type === 'setup_sms') {
const [phone, setPhone] = useState('');
return (
);
}
if (step.type === 'await_approval') {
return (
{step.qrData &&
}`})
}
Waiting for approval...
);
}
if (step.type === 'redirect') {
window.location.href = step.url;
return Redirecting...
;
}
return Step: {step.type}
;
}
return null;
}
```
---
## Web SDK — Vanilla JS Example
```typescript
import { createHawcxAuth } from '@hawcx/core';
const client = createHawcxAuth({ configId: 'your_config_id' });
client.onStateChange(renderUI);
function login(email: string) { client.start(email); }
function renderUI(state) {
const app = document.getElementById('app')!;
if (state.status === 'idle') {
app.innerHTML = `
`;
} else if (state.status === 'loading') {
app.innerHTML = 'Loading...
';
} else if (state.status === 'error') {
app.innerHTML = `Error: ${state.error.message}
`;
} else if (state.status === 'completed') {
fetch('/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ authCode: state.authCode, codeVerifier: state.codeVerifier })
}).then(() => window.location.href = '/');
app.innerHTML = 'Success! Redirecting...
';
} else if (state.status === 'step') {
const { step } = state;
if (step.type === 'select_method') {
app.innerHTML = `Choose method
${step.methods.map(m =>
``).join('')}`;
} else if (step.type === 'enter_code') {
app.innerHTML = `Code sent to ${step.destination}
`;
} else {
app.innerHTML = `Step: ${step.type}
`;
}
}
}
(window as any).login = login;
(window as any).client = client;
```
---
## Node.js Backend SDK — Complete API
### HawcxOAuth
```typescript
import { HawcxOAuth } from '@hawcx/oauth-client';
// Recommended: discovery mode enforces iss + aud. Construct ONCE at startup and
// reuse (one-time discovery fetch); throws DiscoveryError at boot on a bad or
// unreachable issuer.
const oauth = await HawcxOAuth.fromIssuer({
issuer: process.env.HAWCX_BASE_URL!, // your environment's Base URL
configId: process.env.HAWCX_CONFIG_ID!, // sent as the X-Config-Id header
clientId: process.env.HAWCX_CLIENT_ID!, // expected `aud` on issued id_tokens
});
// Legacy (signature-only — no iss/aud check; optional baseUrl defaults to prod):
// const oauth = new HawcxOAuth({ configId, baseUrl, timeout });
```
### `exchangeCode(authCode, codeVerifier)`
```typescript
const { idToken, claims } = await oauth.exchangeCode(authCode, codeVerifier);
// claims.sub = user ID
// claims.email = verified email
```
Returns: `{ idToken: string, claims: { sub: string, email: string, ... } }`
### `verifyToken(token)` — verify a JWT and return claims
```typescript
const claims = await oauth.verifyToken(idToken);
```
### `refreshJwks()` — refresh JWKS cache
```typescript
oauth.refreshJwks(); // clearCache() is a deprecated alias (removed in 6.0.0)
```
### Error Types
```typescript
import { DiscoveryError, TokenExchangeError, TokenVerificationError } from '@hawcx/oauth-client';
// At startup — fromIssuer does the discovery fetch and can throw DiscoveryError:
try {
const oauth = await HawcxOAuth.fromIssuer({ issuer, configId, clientId });
} catch (error) {
if (error instanceof DiscoveryError) {
// Issuer unreachable or discovery document malformed. Fail startup loudly —
// this is a boot-time error, not something a per-request handler can catch.
}
}
// Per request — exchangeCode can throw TokenExchangeError / TokenVerificationError:
try {
const { claims } = await oauth.exchangeCode(authCode, codeVerifier);
} catch (error) {
if (error instanceof TokenExchangeError) {
// Code invalid, expired, or already used. Ask user to re-authenticate.
console.error(error.message, error.statusCode);
}
if (error instanceof TokenVerificationError) {
// JWT signature/claims failed. Log incident, re-authenticate.
console.error(error.message);
}
}
```
### Express.js Integration
```typescript
import express from 'express';
import { HawcxOAuth } from '@hawcx/oauth-client';
const app = express();
app.use(express.json());
const oauth = await HawcxOAuth.fromIssuer({
issuer: process.env.HAWCX_BASE_URL!, configId: process.env.HAWCX_CONFIG_ID!, clientId: process.env.HAWCX_CLIENT_ID!,
});
app.post('/exchange', async (req, res) => {
const { authCode, codeVerifier } = req.body;
if (!authCode || !codeVerifier) {
return res.status(400).json({ error: 'Missing authCode or codeVerifier' });
}
try {
const { claims } = await oauth.exchangeCode(authCode, codeVerifier);
// Create your own session — DO NOT use idToken as your access token
req.session.userId = claims.sub;
req.session.email = claims.email;
// Or use httpOnly cookies (recommended):
// const sessionToken = jwt.sign({ sub: claims.sub, email: claims.email }, SECRET, { expiresIn: '1h' });
// res.cookie('session', sessionToken, { httpOnly: true, secure: true, sameSite: 'strict' });
res.json({ success: true, userId: claims.sub });
} catch (error) {
console.error('Authentication failed:', error);
res.status(401).json({ error: 'Authentication failed' });
}
});
```
### Fastify Integration
```typescript
import Fastify from 'fastify';
import { HawcxOAuth } from '@hawcx/oauth-client';
const app = Fastify();
const oauth = await HawcxOAuth.fromIssuer({
issuer: process.env.HAWCX_BASE_URL!, configId: process.env.HAWCX_CONFIG_ID!, clientId: process.env.HAWCX_CLIENT_ID!,
});
app.post<{ Body: { authCode: string; codeVerifier: string } }>('/exchange', async (request, reply) => {
const { authCode, codeVerifier } = request.body;
try {
const { claims } = await oauth.exchangeCode(authCode, codeVerifier);
reply.send({ success: true, userId: claims.sub });
} catch (error) {
reply.status(401).send({ error: 'Authentication failed' });
}
});
```
### Step-Up Client (MFA Management)
Node's step-up surface is `StepUpClient` — `startToken` and `consumeReceipt` only (there is **no** `managementRequest` on it). A legacy ECIES `DelegationClient` / `MfaMethod` also exists in `@hawcx/oauth-client` (see the Node reference), but `StepUpClient.withPrivateKeyJwt` is preferred.
```typescript
import { StepUpClient } from '@hawcx/oauth-client';
const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!); // OKP/Ed25519, with kid
// Recommended: private_key_jwt (RFC 7523) — reuses your OIDC signing key
const stepUp = StepUpClient.withPrivateKeyJwt({
baseUrl: process.env.HAWCX_BASE_URL!,
configId: process.env.HAWCX_CONFIG_ID!,
clientId: process.env.HAWCX_CLIENT_ID!,
oidcSigningKey: privateJwk,
kid: privateJwk.kid,
});
// Begin a step-up flow
const { start_token, expires_in } = await stepUp.startToken({
userId: 'user@example.com',
purpose: 'change_mfa_method',
newMfaMethod: 'totp', // 'email_otp' | 'sms_otp' | 'totp'
});
// Finalize after the user completes the MFA challenge (receipt from your frontend)
const { ok } = await stepUp.consumeReceipt({ receipt: 'step-up-receipt-from-client' });
```
A legacy `StepUpClient.fromSecretKey({ secretKey: 'hwx_sk_v1_…', baseUrl })` (ECIES blob) also exists but is deprecated — prefer `withPrivateKeyJwt`.
---
## Python Backend SDK — Complete API
### HawcxOAuth
```python
from hawcx_oauth_client import HawcxOAuth
import os
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], # issuer URL (Base URL from Admin Console)
os.environ['HAWCX_CONFIG_ID'],
os.environ['HAWCX_CLIENT_ID'], # expected `aud` on issued id_tokens
)
# NOTE: Python has NO base_url constructor and no single-arg form — from_issuer
# (or HawcxOAuthAsync.from_issuer) is the only public entry point.
```
### `exchange_code(auth_code, code_verifier)`
```python
result = oauth.exchange_code(auth_code, code_verifier)
claims = result.claims
# claims['sub'] = user ID
# claims.get('email') = verified email
# result.id_token = raw JWT (do not use as access token)
```
### `verify_token(token)`
```python
claims = oauth.verify_token(id_token)
```
### Flask Integration
```python
from flask import Flask, request, jsonify, session
from hawcx_oauth_client import HawcxOAuth
import os
app = Flask(__name__)
app.secret_key = os.getenv('FLASK_SECRET_KEY')
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], os.environ['HAWCX_CONFIG_ID'], os.environ['HAWCX_CLIENT_ID']
)
@app.route('/exchange', methods=['POST'])
def exchange():
data = request.json
auth_code = data.get('authCode')
code_verifier = data.get('codeVerifier')
if not auth_code or not code_verifier:
return jsonify({'error': 'Missing authCode or codeVerifier'}), 400
try:
result = oauth.exchange_code(auth_code, code_verifier)
claims = result.claims
session['user_id'] = claims['sub']
session['email'] = claims.get('email')
return jsonify({'success': True, 'userId': claims['sub']})
except Exception as error:
app.logger.error(f'Authentication failed: {error}')
return jsonify({'error': 'Authentication failed'}), 401
```
### Django Integration
```python
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from hawcx_oauth_client import HawcxOAuth
import os
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], os.environ['HAWCX_CONFIG_ID'], os.environ['HAWCX_CLIENT_ID']
)
@require_http_methods(["POST"])
def exchange(request):
auth_code = request.POST.get('authCode')
code_verifier = request.POST.get('codeVerifier')
if not auth_code or not code_verifier:
return JsonResponse({'error': 'Missing authCode or codeVerifier'}, status=400)
try:
result = oauth.exchange_code(auth_code, code_verifier)
claims = result.claims
request.session['user_id'] = claims['sub']
return JsonResponse({'success': True, 'userId': claims['sub']})
except Exception:
return JsonResponse({'error': 'Authentication failed'}, status=401)
```
### FastAPI Integration
```python
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from hawcx_oauth_client import HawcxOAuth
import os
app = FastAPI()
oauth = HawcxOAuth.from_issuer(
os.environ['HAWCX_BASE_URL'], os.environ['HAWCX_CONFIG_ID'], os.environ['HAWCX_CLIENT_ID']
)
@app.post('/exchange')
async def exchange(payload: dict):
auth_code = payload.get('authCode')
code_verifier = payload.get('codeVerifier')
if not auth_code or not code_verifier:
return JSONResponse({'error': 'Missing authCode or codeVerifier'}, status_code=400)
try:
result = oauth.exchange_code(auth_code, code_verifier)
claims = result.claims
return JSONResponse({'success': True, 'userId': claims['sub'], 'email': claims.get('email')})
except Exception:
return JSONResponse({'error': 'Authentication failed'}, status_code=401)
```
### Python Step-Up Client (MFA Management)
Python's management surface is `StepUpClient` — `start_token`, `consume_receipt`, and a
generic `management_request`. There is **no** `HawcxDelegationClient` / `MfaMethod`.
```python
import os
from hawcx_oauth_client import StepUpClient
# Recommended: private_key_jwt (RFC 7523) — reuses your OIDC signing key
client = StepUpClient.with_private_key_jwt(
oidc_signing_key=os.environ['HAWCX_OIDC_PRIVATE_KEY_PEM'], # Ed25519 PEM or bytes
kid=os.environ['HAWCX_PRIVATE_KEY_KID'],
client_id=os.environ['HAWCX_CLIENT_ID'],
base_url=os.environ['HAWCX_BASE_URL'],
config_id=os.environ['HAWCX_CONFIG_ID'],
)
# Begin a step-up flow
result = client.start_token(
user_id='user@example.com',
purpose='change_mfa_method',
new_mfa_method='sms_otp', # 'sms_otp' | 'email_otp' | 'totp'
)
# Finalize once the user completes the MFA challenge (receipt from your frontend)
client.consume_receipt(receipt='receipt-from-frontend')
```
---
## Java Backend SDK (Complete API)
### HawcxOauthClient
```java
import com.hawcx.oauth.HawcxOauthClient;
import com.hawcx.oauth.HawcxOauthConfig;
import com.hawcx.oauth.JwtVerifier;
import com.hawcx.oauth.TokenRequest;
HawcxOauthConfig config = new HawcxOauthConfig(System.getenv("HAWCX_BASE_URL"));
HawcxOauthClient client = new HawcxOauthClient(config);
```
### `exchangeCodeForClaims(TokenRequest, VerifyOptions)`
```java
TokenRequest req = new TokenRequest(configId, authCode, codeVerifier);
JwtVerifier.VerifyOptions opts = JwtVerifier.VerifyOptions.builder()
.jwksUrl(config.keysEndpoint())
.audience(System.getenv("HAWCX_CONFIG_ID"))
.issuer(System.getenv("HAWCX_BASE_URL"))
.leewaySeconds(10)
.build();
JsonObject claims = client.exchangeCodeForClaims(req, opts);
String userId = claims.get("sub").getAsString();
```
### `verifyJwt(token, opts)`
```java
JsonObject claims = client.verifyJwt(idToken, opts);
```
### Spring Boot Integration
```java
@RestController
public class AuthController {
private final HawcxOauthClient client;
private final JwtVerifier.VerifyOptions verifyOpts;
private final String configId;
public AuthController(@Value("${hawcx.base-url}") String baseUrl,
@Value("${hawcx.config-id}") String configId) {
this.configId = configId;
HawcxOauthConfig config = new HawcxOauthConfig(baseUrl);
this.client = new HawcxOauthClient(config);
this.verifyOpts = JwtVerifier.VerifyOptions.builder()
.jwksUrl(config.keysEndpoint())
.audience(configId)
.issuer(baseUrl)
.leewaySeconds(10)
.build();
}
public record ExchangeRequest(String authCode, String codeVerifier) {}
@PostMapping("/exchange")
public ResponseEntity> exchange(@RequestBody ExchangeRequest body) {
try {
TokenRequest req = new TokenRequest(configId, body.authCode(), body.codeVerifier());
JsonObject claims = client.exchangeCodeForClaims(req, verifyOpts);
return ResponseEntity.ok(Map.of(
"success", true,
"userId", claims.get("sub").getAsString()
));
} catch (Exception e) {
return ResponseEntity.status(401).body(Map.of("error", "Authentication failed"));
}
}
}
```
### Java Step-Up Client (MFA Management)
Java's management surface is `StepUpClient` — `startToken`, `consumeReceipt`,
`getSigninPolicy`, `getMfaEnforcement`, `setMfaEnforcement`. Prefer
`withPrivateKeyJwt` (RFC 7523). A legacy `StepUpClient.fromSecretKey(baseUrl,
"hwx_sk_v1_…", configId)` (ECIES blob) also exists but is deprecated.
```java
import com.hawcx.delegation.StepUpClient;
import com.hawcx.management.StartTokenResponse;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
// Load your Ed25519 OIDC signing key (same one registered in the Admin Console)
String pem = System.getenv("HAWCX_OIDC_PRIVATE_KEY_PEM")
.replaceAll("-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\\s", "");
PrivateKey privateKey = KeyFactory.getInstance("Ed25519")
.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pem)));
StepUpClient client = StepUpClient.withPrivateKeyJwt(
System.getenv("HAWCX_BASE_URL"),
System.getenv("HAWCX_CONFIG_ID"),
privateKey,
System.getenv("HAWCX_PRIVATE_KEY_KID"),
System.getenv("HAWCX_CLIENT_ID"));
// Begin a step-up flow (mfaMethod: "email_otp" | "sms_otp" | "totp")
StartTokenResponse token = client.startToken(
"user@example.com", "change_mfa_method", "totp");
// Finalize after the user completes the MFA challenge (receipt from your frontend)
client.consumeReceipt("receipt-from-frontend");
```
---
## Android Native SDK (Kotlin)
### Installation
```groovy
// build.gradle
dependencies {
implementation 'com.hawcx:hawcx-android-sdk:4.0.0'
}
```
### Initialize
```kotlin
import com.hawcx.internal.HawcxSDK
class MainApplication : Application() {
companion object {
lateinit var hawcxSdkInstance: HawcxSDK
private set
}
override fun onCreate() {
super.onCreate()
hawcxSdkInstance = HawcxSDK(
context = this.applicationContext,
projectApiKey = "YOUR_CONFIG_ID"
)
}
}
```
### Hawcx Authentication
Hawcx auto-detects new vs returning users. Single entry point — no separate signup/signin.
```kotlin
class AuthManager : AuthV4Callback {
private val sdk = MainApplication.hawcxSdkInstance
fun smartConnect(email: String) {
sdk.authenticateV4(userid = email, callback = this)
}
fun submitOTP(otp: String) {
sdk.submitOtpV4(otp = otp)
}
override fun onOtpRequired() {
// Show OTP input UI
}
override fun onAuthSuccess(accessToken: String?, refreshToken: String?, isLoginFlow: Boolean) {
if (isLoginFlow) {
// Fully authenticated — navigate to home
} else {
// Device registration phase — SDK continues automatically
}
}
override fun onError(errorCode: AuthV4ErrorCode, errorMessage: String) {
// Handle error
}
}
```
### SDK Methods (Android)
- `authenticateV4(userid: String, callback: AuthV4Callback)` — initiate authentication
- `submitOtpV4(otp: String)` — submit OTP when required
- `cancelV4Auth()` — cancel ongoing authentication
### SDK Callbacks (AuthV4Callback)
- `onOtpRequired()` — new user/device needs verification
- `onAuthSuccess(accessToken, refreshToken, isLoginFlow)` — authentication succeeded
- `onError(errorCode, errorMessage)` — error occurred
### Device Session Management (Android)
- `clearSessionTokens(userId)` — standard logout, keeps device trusted (no OTP on next login)
- `clearUserData(userId)` — full device removal, user must re-verify with OTP
- `getLastLoggedInUser()` — returns last authenticated email (for pre-fill)
- `clearLastLoggedInUser()` — clear pre-fill record
### Error Codes (Android AuthV4ErrorCode)
| Code | Description |
|------|-------------|
| `networkError` | Network connectivity issue |
| `invalidInput` | Invalid email format |
| `keychainSaveFailed` | Failed to save to Keystore |
| `clientCryptoError` | Cryptographic operation failed |
| `fingerprintError` | Device fingerprint failed |
| `authInitFailed` | Auth initialization failed |
| `otpVerificationFailed` | Invalid or expired OTP |
| `deviceVerificationFailed` | Device registration verification failed |
| `missingCryptoForLogin` | Device keys not found |
---
## iOS Native SDK (Swift)
### Installation (SPM)
```swift
dependencies: [
.package(url: "https://github.com/hawcx/hawcx_ios_sdk", .upToNextMajor(from: "4.0.2"))
]
```
### Initialize
```swift
import HawcxFramework
let hawcxSDK = HawcxSDK(projectApiKey: "YOUR_CONFIG_ID")
```
### Hawcx Authentication
```swift
class AuthManager: NSObject, AuthV4Callback {
private let sdk = HawcxSDK(projectApiKey: "YOUR_CONFIG_ID")
func smartConnect(email: String) {
sdk.authenticateV4(userid: email, callback: self)
}
func submitOTP(otp: String) {
sdk.submitOtpV4(otp: otp)
}
func onOtpRequired() {
// Show OTP input UI
}
func onAuthSuccess(accessToken: String?, refreshToken: String?, isLoginFlow: Bool) {
if isLoginFlow {
// Fully authenticated
} else {
// Device registration phase
}
}
func onError(errorCode: AuthV4ErrorCode, errorMessage: String) {
// Handle error
}
}
```
### Web Login Approval (iOS)
```swift
// Step 1: Validate PIN from QR code
sdk.webLogin(pin: pin, callback: self) // WebLoginCallback
// Step 2: Approve login
let webToken = UserDefaults.standard.string(forKey: "web_token")!
sdk.webApprove(token: webToken, callback: self)
// Callbacks: onSuccess(), showError(webLoginErrorCode:errorMessage:)
```
### Device Session Management (iOS)
- `clearSessionTokens(forUser: userId)` — logout, keep device trusted
- `clearUserKeychainData(forUser: userId)` — full device removal (destructive)
- `getLastLoggedInUser() -> String` — last authenticated email
- `clearLastLoggedInUser()` — clear pre-fill
### Error Codes (iOS AuthV4ErrorCode)
| Code | Description |
|------|-------------|
| `networkError` | Network issue or timeout |
| `invalidInput` | Invalid email format |
| `keychainSaveFailed` | Failed to save to Keychain |
| `clientCryptoError` | Cryptographic operation failed |
| `fingerprintError` | Device fingerprint failed |
| `otpVerificationFailed` | Invalid or expired OTP |
| `missingCryptoForLogin` | Device keys not found |
| `internalStateError` | SDK state corruption |
### iOS WebLoginErrorCode
| Code | Description |
|------|-------------|
| `invalidPin` | 7-digit PIN invalid or expired |
| `failedApprove` | Approval failed |
| `networkError` | Network issue |
---
## React Native SDK
### Installation
```bash
npm install @hawcx/react-native-sdk@1.1.5
cd ios && pod install # iOS
# Android: add Hawcx Maven repo to settings.gradle (see below)
```
Android Maven setup:
```kts
// android/settings.gradle(.kts)
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://raw.githubusercontent.com/hawcx/hawcx_android_sdk/main/maven")
metadataSources { mavenPom(); artifact() }
}
}
}
```
### Initialize
```tsx
import { initialize, addAuthListener } from '@hawcx/react-native-sdk';
await initialize({
projectApiKey: 'YOUR_CONFIG_ID',
baseUrl: 'https://your-tenant.hawcx-api.hawcx.com',
});
```
### Authentication (React Native)
```tsx
import { useHawcxAuth, storeBackendOAuthTokens } from '@hawcx/react-native-sdk';
function SmartConnectScreen() {
const { state, authenticate, submitOtp, reset } = useHawcxAuth();
const [email, setEmail] = useState('');
const [otp, setOtp] = useState('');
useEffect(() => {
if (state.status === 'authorization_code' && email.trim()) {
exchangeCode(email.trim(), state.payload)
.catch(console.error)
.finally(() => reset());
}
}, [state, email, reset]);
return (
{state.status === 'otp' ? (
<>
);
}
async function exchangeCode(userId, payload) {
const response = await fetch('https://your-backend/api/hawcx/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: payload.code, email: userId, expires_in: payload.expiresIn }),
});
const { access_token, refresh_token } = await response.json();
await storeBackendOAuthTokens(userId, access_token, refresh_token);
}
```
### React Native SDK Methods
- `initialize(config)` — configure native instance
- `authenticate(userId)` — begin authentication
- `submitOtp(code)` — forward OTP
- `addAuthListener(listener)` — receive events
- `useHawcxAuth()` — React hook (state, authenticate, submitOtp, reset)
- `storeBackendOAuthTokens(userId, accessToken, refreshToken?)` — persist backend tokens
- `hawcxClient.authenticate(userId, options)` — promise-based helper
### React Native Auth Events
- `otp_required` — show OTP UI
- `authorization_code` — send code to backend
- `additional_verification_required` — step-up needed
- `auth_error` — error occurred
---
## Flutter SDK
### Installation
```yaml
# pubspec.yaml
dependencies:
hawcx_flutter_sdk: ^1.0.5
```
```bash
flutter pub get
cd ios && pod install # iOS
# Android: add Hawcx Maven repo to settings.gradle (same as React Native)
```
Requirements: Flutter 3.19+, iOS 17+, Android 8+ (minSdk 26).
### Initialize
```dart
import 'package:hawcx_flutter_sdk/hawcx_flutter_sdk.dart';
final client = HawcxClient();
await client.initialize(HawcxConfig(
projectApiKey: 'YOUR_CONFIG_ID',
baseUrl: 'https://your-tenant.hawcx-api.hawcx.com',
));
```
### Authentication (Flutter)
```dart
Future startSmartConnect(String email) async {
final auth = client.authenticate(
userId: email.trim(),
onOtpRequired: () {
// Show OTP UI
},
onAuthorizationCode: (payload) async {
final response = await backendExchangeCode(
email: email.trim(),
code: payload.code,
expiresIn: payload.expiresIn,
);
await client.storeBackendOAuthTokens(
userId: email.trim(),
accessToken: response.accessToken,
refreshToken: response.refreshToken,
);
},
onAdditionalVerificationRequired: (payload) {
// Handle step-up
},
);
try {
final result = await auth.future;
// result.isLoginFlow, result.accessToken, result.refreshToken
} catch (error) {
// Handle HawcxAuthException
}
}
// Submit OTP when required:
await client.submitOtp(otpCode.trim());
```
### Flutter SDK Methods
- `client.initialize(config)` — configure native instance
- `client.authenticate(userId, ...)` — begin authentication, returns `HawcxAuthHandle`
- `client.submitOtp(otp)` — forward OTP
- `client.storeBackendOAuthTokens(userId, accessToken, refreshToken?)` — persist tokens
---
## Admin Console Configuration
### Projects
- Create projects from the dashboard (Customer Admin role required)
- Each project has its own Config IDs, flow config, and analytics
- Environments: Development (available immediately) and Production (provisioned by the Hawcx team on request)
### Config IDs
- Public identifiers that connect your app to Hawcx
- Scoped per project and environment (Development / Production)
- Shown in the Admin Console under Project > Settings (no generation step; the development Config ID appears as soon as the project is created)
- Select platform: Web, iOS, or Android
### Flow Configuration
Each auth flow has 3 phases:
1. **Primary Verification** — how users prove identity (required)
- Email OTP, SMS OTP, Magic Link, QR Code, Push
2. **MFA** — optional second factor
- Policy: Off / Optional / Required
- Methods: Email OTP, SMS OTP, TOTP, QR Code, Push
3. **Device Trust** — remember trusted devices
- Skip MFA on trusted devices (on by default)
Configure signup and signin flows independently.
### Team & RBAC
- **Customer Admin**: full access (create projects, manage team, configure projects, billing)
- **Developer**: access to assigned projects (view analytics and Config IDs)
- **Viewer**: read-only access to assigned projects
---
## Troubleshooting
### Frontend: State stuck on "loading"
The SDK handles `setup_device` and `device_challenge` steps internally. If loading persists >15s: check network, verify apiBase URL, check console.
### Frontend: Unknown step type
Always include a default case in your step handler.
### Backend: "Authentication failed" on exchange
- Codes expire in 60 seconds — exchange immediately
- Codes are single-use — don't retry with same code
- Send BOTH `authCode` AND `codeVerifier` (PKCE required)
### React: `useAuthState()` returns undefined
Component must be wrapped in `HawcxProvider`.
### React: Side effects in render
Use `useEffect` for the completed state, not inline fetch calls.
### CORS errors
Local development over `http://localhost` works without any origin setup — the Hawcx auth API (`/v1/*`) accepts cross-origin browser requests. Run the code exchange (`/oauth2/token`) on your backend, never in the browser.
---
## Security Best Practices
1. **Always verify on backend** — never trust frontend state for authorization
2. **Don't use idToken as access token** — create your own session token from claims
3. **Use httpOnly cookies** for session storage:
```typescript
res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' });
```
4. **HTTPS required** — the SDK will fail on HTTP in production
5. **Exchange codes immediately** — they expire in 60 seconds
6. **Backend secrets stay on backend** — never expose `HAWCX_SECRET_KEY` to frontend/mobile
---
## Support
- Community Slack: https://docs.hawcx.com/docs/v1/support
- Email: support@hawcx.com
- Docs: https://docs.hawcx.com