Headless Authentication
Headless authentication enables programmatic authentication with the Cartridge Controller SDK without displaying any user interface. This is ideal for automated workflows, server-side applications, and creating seamless user experiences where you want to minimize UI interruptions.
Overview
Headless mode works by:
- Passing credentials directly to
controller.connect({ username, signer, password? }) - Performing authentication in a hidden iframe without opening any modal
- Only showing UI if session policies require explicit user approval
- Returning the authenticated account for immediate use
Controller SDK → Hidden Keychain iframe → Backend API → Authenticated AccountBasic Usage
WebAuthn/Passkey Authentication
The most secure option for headless authentication uses WebAuthn (passkeys):
import Controller from "@cartridge/controller";
const controller = new Controller({});
try {
const account = await controller.connect({
username: "alice",
signer: "webauthn",
});
console.log("Authenticated successfully:", account.address);
} catch (error) {
console.error("Authentication failed:", error.message);
}Password Authentication
For scenarios where WebAuthn isn't available:
const account = await controller.connect({
username: "alice",
signer: "password",
password: "your-secure-password",
});OAuth Providers
Authenticate using social login providers:
// Google OAuth
await controller.connect({
username: "alice",
signer: "google"
});
// Discord OAuth
await controller.connect({
username: "alice",
signer: "discord"
});EVM Wallet Authentication
Connect using Ethereum wallets via EIP-191 signing:
// MetaMask
await controller.connect({
username: "alice",
signer: "metamask"
});
// Phantom EVM
await controller.connect({
username: "alice",
signer: "phantom-evm"
});
// Rabby Wallet
await controller.connect({
username: "alice",
signer: "rabby"
});WalletConnect
For mobile wallet connections:
await controller.connect({
username: "alice",
signer: "walletconnect"
});Supported Signer Options
Headless mode supports all implemented authentication methods:
webauthn- WebAuthn/Passkey (most secure)password- Username/password authenticationgoogle- Google OAuthdiscord- Discord OAuthmetamask- MetaMask walletrabby- Rabby walletphantom-evm- Phantom EVM walletwalletconnect- WalletConnect protocol
Session Approval Flow
If your application uses session policies that haven't been verified or include spending limits that require approval, the keychain will automatically open the approval UI after successful authentication:
const controller = new Controller({
policies: {
contracts: {
"0x1234...": {
name: "My Game Contract",
methods: [{ name: "play", entrypoint: "play" }],
},
},
},
});
// This may open session approval UI after authentication
const account = await controller.connect({
username: "alice",
signer: "webauthn",
});
// Account is ready to use once connect() resolves
await account.execute(/* your transaction */);Error Handling
Headless authentication provides specific error handling:
import { HeadlessAuthenticationError } from "@cartridge/controller";
try {
const account = await controller.connect({
username: "alice",
signer: "webauthn",
});
} catch (error) {
if (error instanceof HeadlessAuthenticationError) {
// Handle authentication-specific errors
console.error("Auth failed:", error.message);
// Common reasons:
// - Username doesn't exist
// - Signer not associated with username
// - Invalid credentials
// - Network connectivity issues
} else {
// Handle other errors
console.error("Unexpected error:", error);
}
}Integration Patterns
React Hook Pattern
Create a reusable hook for headless authentication:
import { useCallback, useState } from 'react';
import { useConnect } from '@starknet-react/core';
import { ControllerConnector } from '@cartridge/connector';
export function useHeadlessAuth() {
const { connectAsync, connectors } = useConnect();
const [loading, setLoading] = useState(false);
const controller = connectors[0] as ControllerConnector;
const authenticateHeadless = useCallback(async (
username: string,
signer: string
) => {
setLoading(true);
try {
// Disconnect if already connected
if (controller.account) {
await controller.disconnect();
}
// Headless authentication
const account = await controller.connect({ username, signer });
if (!account) {
throw new Error('Authentication failed');
}
// Sync with starknet-react
await connectAsync({ connector: controller });
return account;
} finally {
setLoading(false);
}
}, [controller, connectAsync]);
return { authenticateHeadless, loading };
}Server-Side Pattern (Node.js)
For server-side applications, use the SessionProvider:
import { SessionProvider } from "@cartridge/connector";
const sessionProvider = new SessionProvider({
rpc: "https://api.cartridge.gg/x/starknet/mainnet",
chainId: "SN_MAIN",
// Note: SessionProvider doesn't support headless mode directly
// Use regular browser-based headless authentication for programmatic flows
});Security Considerations
Credential Storage
- Never commit credentials to source code
- Use environment variables for production credentials
- Consider secure key management systems for sensitive applications
- Rotate credentials regularly
Authentication Method Selection
- WebAuthn (recommended): Most secure, hardware-backed when available
- OAuth: Good for user convenience, relies on third-party security
- Password: Least secure, but widely compatible
- EVM Wallets: Security depends on wallet implementation and user practices
Session Management
// Always handle session lifecycle properly
const account = await controller.connect({ username, signer });
// Use the account for transactions
await account.execute(calls);
// Clean up when done
await controller.disconnect();Differences from Native Headless Mode
This web-based headless authentication is different from the native headless Controller:
| Feature | Web Headless Authentication | Native Headless Controller |
|---|---|---|
| Environment | Browser applications | Server-side, native apps |
| Implementation | Hidden iframe + postMessage | Direct API integration |
| Key Management | Managed by Cartridge keychain | Application-managed private keys |
| UI Fallback | Can open UI for approvals | No UI available |
| Use Case | Seamless web UX | Backend services, automation |
Troubleshooting
Common Issues
- "User not found": Username doesn't exist in the system
- "Signer not found": The specified signer isn't associated with the username
- "Not ready to connect": Controller initialization is still in progress
- Network timeouts: Check network connectivity and RPC endpoint availability
Debug Mode
Enable debug logging to troubleshoot issues:
// Enable debug mode in development
const controller = new Controller({
// Add debug configuration if available
});
// Check browser console for detailed error messagesValidation
Validate inputs before attempting authentication:
function validateHeadlessOptions(username: string, signer: string) {
if (!username || username.trim().length === 0) {
throw new Error("Username is required");
}
const validSigners = [
"webauthn", "password", "google", "discord",
"metamask", "rabby", "phantom-evm", "walletconnect"
];
if (!validSigners.includes(signer)) {
throw new Error(`Invalid signer: ${signer}`);
}
}Next Steps
- Learn about Session Policies for fine-grained transaction control
- Explore React integration patterns for web applications
- Consider native headless mode for backend services
- Set up error handling and logging for production use