Skip to content Skip to footer

WebAuthn: The Complete Guide to Modern Passwordless Authentication

WebAuthn

Passwords have been the Achilles’ heel of digital security for decades. Despite countless breaches, credential stuffing attacks, and phishing campaigns that exploit password vulnerabilities, organizations continue to rely on this fundamentally flawed authentication method. In 2024, over 80% of data breaches still involve compromised credentials, with the average breach costing organizations $4.88 million.

WebAuthn (Web Authentication) represents a paradigm shift in how we approach online authentication. As a W3C standard developed in collaboration with the FIDO Alliance, WebAuthn enables websites and applications to implement strong, passwordless authentication using public-key cryptography. Instead of transmitting secrets that can be intercepted, WebAuthn uses cryptographic key pairs where the private key never leaves the user’s device.

This comprehensive guide answers the fundamental questions: what is WebAuthn, how does WebAuthn work, and provides practical guidance including how do I add WebAuthn passkeys to a dApp. Whether you’re a security professional evaluating authentication options, a developer implementing modern authentication, or an IT leader planning your organization’s passwordless journey, this guide provides the technical depth and practical insights you need.

What Is WebAuthn?

WebAuthn (Web Authentication API) is a web standard published by the World Wide Web Consortium (W3C) that enables strong authentication for web applications. It provides a JavaScript API that allows websites to create and use public-key credentials for user authentication, eliminating the need for passwords.

The Core Concept

At its heart, WebAuthn replaces the traditional “something you know” (passwords) with “something you have” (an authenticator device) combined with “something you are” or “something you know” (biometric verification or PIN on the device).

When you authenticate with WebAuthn:

  • Your device generates a unique cryptographic key pair for each website
  • The private key stays securely stored in hardware (never transmitted)
  • The public key is shared with the website
  • Authentication involves proving you possess the private key without revealing it

WebAuthn Within the FIDO2 Framework

WebAuthn is one component of the broader FIDO2 standard, which consists of two complementary specifications:

FIDO2 Standard Components:

WebAuthn (W3C Web Standard)

  • Browser JavaScript API
  • Server-side validation
  • Credential management

CTAP (Client to Authenticator Protocol)

  • USB communication
  • NFC communication
  • Bluetooth communication
  • Platform authenticator interface



  • WebAuthn: The browser-side API that web applications use
  • CTAP (Client to Authenticator Protocol): How browsers communicate with authenticators

Key Terminology

Term

Definition

Relying Party (RP)

The website or application implementing WebAuthn authentication

Authenticator

Device that generates and stores credentials (security key, phone, laptop)

Credential

The public/private key pair created for a specific relying party

User Verification

Local authentication on the device (biometric, PIN)

User Presence

Physical interaction proving a human is present (button press, tap)

Attestation

Cryptographic proof of the authenticator’s identity and properties

Passkey

Consumer-friendly term for WebAuthn credentials, especially synced ones

Types of Authenticators

Platform Authenticators (Built-in)

  • Windows Hello (fingerprint, face, PIN)
  • Apple Touch ID / Face ID
  • Android biometrics
  • Integrated into the device, always available

Roaming Authenticators (External)

  • USB security keys (YubiKey, Titan, Feitian)
  • NFC security keys
  • Bluetooth security keys
  • Portable across devices

Hybrid Authenticators

  • Smartphone as authenticator for computer login
  • Cross-device authentication via QR code + Bluetooth
  • Combines convenience with security

How Does WebAuthn Work?

Understanding how WebAuthn works requires examining its two core ceremonies: registration (creating credentials) and authentication (using credentials).

The Registration Ceremony

Registration occurs when a user first sets up WebAuthn authentication with a website.

Step-by-Step Flow:

Step-by-Step Registration Process:

  1. User → Clicks “Register”
  2. Browser → Requests registration options from server
  3. Server → Returns challenge + user info
  4. Browser → Calls navigator.credentials.create()
  5. Authenticator → Prompts user to verify identity (biometric/PIN)
  6. User → Approves registration
  7. Authenticator → Generates key pair, signs challenge
  8. Browser → Sends public key + attestation to server
  9. Server → Verifies and stores public key
  10. Server → Returns registration complete confirmation

 

Technical Details:

  1. Server generates options: Includes challenge (random bytes), relying party info, user info, and acceptable authenticator types

  2. Browser calls WebAuthn API:

const credential = await navigator.credentials.create({

  publicKey: {

    challenge: new Uint8Array([/* server-generated random bytes */]),

    rp: {

      name: “Example Corp”,

      id: “example.com”

    },

    user: {

      id: new Uint8Array([/* unique user identifier */]),

      name: “[email protected]”,

      displayName: “John Doe”

    },

    pubKeyCredParams: [

      { type: “public-key”, alg: -7 },  // ES256

      { type: “public-key”, alg: -257 } // RS256

    ],

    authenticatorSelection: {

      authenticatorAttachment: “platform”, // or “cross-platform”

      userVerification: “required”,

      residentKey: “required” // for passkeys

    },

    timeout: 60000,

    attestation: “direct” // or “none”, “indirect”

  }

});

 

  1. Authenticator generates keys: Creates a unique public/private key pair bound to the relying party’s origin

  2. Private key storage: Private key is stored in secure hardware (TPM, Secure Enclave, security key)

  3. Server receives: Public key, credential ID, and optional attestation for verification and storage

The Authentication Ceremony

Authentication occurs when a user logs in using their registered credential.

Step-by-Step Flow:

User → Clicks “Sign In”

Browser → Requests authentication options from server

Server → Returns challenge + allowed credentials

Browser → Calls navigator.credentials.get()

Authenticator → Prompts user to verify identity (biometric/PIN)

User → Approves authentication

Authenticator → Signs challenge with private key

Browser → Sends signed assertion to server

Server → Verifies signature with stored public key

Server → Returns authentication successful



Authentication API Call:

const assertion = await navigator.credentials.get({

  publicKey: {

    challenge: new Uint8Array([/* server-generated random bytes */]),

    rpId: “example.com”,

    allowCredentials: [

      {

        type: “public-key”,

        id: new Uint8Array([/* credential ID from registration */]),

        transports: [“usb”, “nfc”, “ble”, “internal”]

      }

    ],

    userVerification: “required”,

    timeout: 60000

  }

});

 

Why This Is Secure: The Cryptographic Foundation

Public-Key Cryptography

  • Each credential is a unique key pair (public + private)
  • Private key never leaves the authenticator
  • Server only stores the public key
  • Even if server is breached, credentials cannot be used elsewhere

Origin Binding

  • Credentials are cryptographically bound to the relying party’s origin
  • A credential for bank.com cannot be used on evil-bank.com
  • This binding is enforced by the browser, not user judgment

Challenge-Response

  • Each authentication uses a fresh, random challenge
  • Signed responses cannot be replayed
  • No static secrets are transmitted

Phishing Resistance

  • The combination of origin binding and challenge-response makes WebAuthn inherently phishing-resistant MFA
  • Even if a user visits a phishing site, their credential cannot be used against the legitimate site

How Do I Add WebAuthn Passkeys to a dApp?

Integrating WebAuthn into decentralized applications (dApps) presents unique opportunities and challenges. This section provides practical guidance on how to add WebAuthn passkeys to a dApp.

Why WebAuthn for dApps?

Traditional dApps rely on wallet signatures for authentication, which has limitations:

  • Users must have a wallet installed
  • Private key management is complex
  • Transaction signing UX is often confusing
  • Mobile experience varies significantly

WebAuthn passkeys offer:

  • Native browser support without extensions
  • Familiar biometric authentication UX
  • Hardware-backed key security
  • Cross-platform compatibility

dApp Frontend Components:

  • WebAuthn API Integration  –  Handles credential creation and authentication
  • Web3 Library (ethers.js, wagmi)  –  Blockchain interactions
  • UI Components  –  User interface elements

Backend Services:

  • Backend Server
    • Challenge generation
    • Credential storage
    • Session management
    • Signature verification
  • Blockchain
    • Smart contracts
    • Account abstraction
    • Passkey wallet



Implementation Approaches

Approach 1: WebAuthn for dApp Authentication (Off-chain)

Use WebAuthn for authenticating users to your dApp backend, separate from blockchain transactions.

// Frontend: Registration

async function registerPasskey(username) {

  // Get registration options from your backend

  const optionsResponse = await fetch(‘/api/webauthn/register/options’, {

    method: ‘POST’,

    headers: { ‘Content-Type’: ‘application/json’ },

    body: JSON.stringify({ username })

  });

  const options = await optionsResponse.json();

  

  // Convert base64 strings to ArrayBuffers

  options.challenge = base64ToArrayBuffer(options.challenge);

  options.user.id = base64ToArrayBuffer(options.user.id);

  

  // Create credential

  const credential = await navigator.credentials.create({

    publicKey: options

  });

  

  // Send credential to backend for verification and storage

  const verifyResponse = await fetch(‘/api/webauthn/register/verify’, {

    method: ‘POST’,

    headers: { ‘Content-Type’: ‘application/json’ },

    body: JSON.stringify({

      id: credential.id,

      rawId: arrayBufferToBase64(credential.rawId),

      response: {

        attestationObject: arrayBufferToBase64(

          credential.response.attestationObject

        ),

        clientDataJSON: arrayBufferToBase64(

          credential.response.clientDataJSON

        )

      },

      type: credential.type

    })

  });

  

  return await verifyResponse.json();

}

 

// Frontend: Authentication

async function authenticateWithPasskey() {

  // Get authentication options

  const optionsResponse = await fetch(‘/api/webauthn/authenticate/options’, {

    method: ‘POST’

  });

  const options = await optionsResponse.json();

  

  options.challenge = base64ToArrayBuffer(options.challenge);

  options.allowCredentials = options.allowCredentials.map(cred => ({

    …cred,

    id: base64ToArrayBuffer(cred.id)

  }));

  

  // Get assertion

  const assertion = await navigator.credentials.get({

    publicKey: options

  });

  

  // Verify with backend

  const verifyResponse = await fetch(‘/api/webauthn/authenticate/verify’, {

    method: ‘POST’,

    headers: { ‘Content-Type’: ‘application/json’ },

    body: JSON.stringify({

      id: assertion.id,

      rawId: arrayBufferToBase64(assertion.rawId),

      response: {

        authenticatorData: arrayBufferToBase64(

          assertion.response.authenticatorData

        ),

        clientDataJSON: arrayBufferToBase64(

          assertion.response.clientDataJSON

        ),

        signature: arrayBufferToBase64(

          assertion.response.signature

        )

      },

      type: assertion.type

    })

  });

  

  const result = await verifyResponse.json();

  if (result.verified) {

    // User authenticated – create session, etc.

  }

}

 

Approach 2: Passkey-Based Smart Contract Wallets (On-chain)

Modern account abstraction (ERC-4337) enables passkeys to directly control smart contract wallets.

// Using a passkey-enabled smart wallet SDK (example structure)

import { PasskeyWallet } from ‘@example/passkey-wallet-sdk’;

 

async function createPasskeyWallet() {

  // Create WebAuthn credential

  const credential = await navigator.credentials.create({

    publicKey: {

      challenge: crypto.getRandomValues(new Uint8Array(32)),

      rp: { name: “My dApp”, id: window.location.hostname },

      user: {

        id: crypto.getRandomValues(new Uint8Array(16)),

        name: userEmail,

        displayName: userName

      },

      pubKeyCredParams: [

        { type: “public-key”, alg: -7 } // ES256 (P-256)

      ],

      authenticatorSelection: {

        residentKey: “required”,

        userVerification: “required”

      }

    }

  });

  

  // Extract public key from attestation

  const publicKey = extractPublicKey(credential.response.attestationObject);

  

  // Deploy smart contract wallet with passkey as signer

  const wallet = await PasskeyWallet.create({

    publicKey: publicKey,

    credentialId: credential.id,

    network: ‘mainnet’

  });

  

  return wallet;

}

 

async function signTransaction(wallet, transaction) {

  // Get signature challenge from wallet

  const challenge = await wallet.getSignatureChallenge(transaction);

  

  // Sign with passkey

  const assertion = await navigator.credentials.get({

    publicKey: {

      challenge: challenge,

      rpId: window.location.hostname,

      allowCredentials: [{

        type: “public-key”,

        id: base64ToArrayBuffer(wallet.credentialId)

      }],

      userVerification: “required”

    }

  });

  

  // Submit signed transaction

  const txHash = await wallet.submitTransaction(transaction, {

    signature: assertion.response.signature,

    authenticatorData: assertion.response.authenticatorData,

    clientDataJSON: assertion.response.clientDataJSON

  });

  

  return txHash;

}

 

Backend Implementation (Node.js Example)

// Using @simplewebauthn/server library

import {

  generateRegistrationOptions,

  verifyRegistrationResponse,

  generateAuthenticationOptions,

  verifyAuthenticationResponse

} from ‘@simplewebauthn/server’;

 

const rpName = ‘My dApp’;

const rpID = ‘mydapp.com’;

const origin = `https://${rpID}`;

 

// Registration options endpoint

app.post(‘/api/webauthn/register/options’, async (req, res) => {

  const { username } = req.body;

  

  // Get user from database or create new

  const user = await findOrCreateUser(username);

  

  // Get existing credentials to exclude

  const existingCredentials = await getUserCredentials(user.id);

  

  const options = await generateRegistrationOptions({

    rpName,

    rpID,

    userID: user.id,

    userName: username,

    userDisplayName: user.displayName || username,

    attestationType: ‘none’,

    excludeCredentials: existingCredentials.map(cred => ({

      id: cred.credentialID,

      type: ‘public-key’,

      transports: cred.transports

    })),

    authenticatorSelection: {

      residentKey: ‘required’,

      userVerification: ‘required’

    }

  });

  

  // Store challenge for verification

  await storeChallenge(user.id, options.challenge);

  

  res.json(options);

});

 

// Registration verification endpoint

app.post(‘/api/webauthn/register/verify’, async (req, res) => {

  const { body } = req;

  const user = await getCurrentUser(req);

  const expectedChallenge = await getStoredChallenge(user.id);

  

  const verification = await verifyRegistrationResponse({

    response: body,

    expectedChallenge,

    expectedOrigin: origin,

    expectedRPID: rpID

  });

  

  if (verification.verified && verification.registrationInfo) {

    const { credentialPublicKey, credentialID, counter } = 

      verification.registrationInfo;

    

    // Store credential in database

    await storeCredential({

      credentialID,

      credentialPublicKey,

      counter,

      userId: user.id,

      transports: body.response.transports

    });

  }

  

  res.json({ verified: verification.verified });

});

 

// Authentication verification endpoint

app.post(‘/api/webauthn/authenticate/verify’, async (req, res) => {

  const { body } = req;

  

  // Find credential in database

  const credential = await findCredentialById(body.id);

  const expectedChallenge = await getStoredChallenge(credential.userId);

  

  const verification = await verifyAuthenticationResponse({

    response: body,

    expectedChallenge,

    expectedOrigin: origin,

    expectedRPID: rpID,

    authenticator: {

      credentialID: credential.credentialID,

      credentialPublicKey: credential.credentialPublicKey,

      counter: credential.counter

    }

  });

  

  if (verification.verified) {

    // Update counter to prevent replay attacks

    await updateCredentialCounter(

      credential.id, 

      verification.authenticationInfo.newCounter

    );

    

    // Create session

    const session = await createUserSession(credential.userId);

    res.cookie(‘session’, session.token, { httpOnly: true, secure: true });

  }

  

  res.json({ verified: verification.verified });

});

 

Key Considerations for dApp Integration

  1. Credential Storage
  • Server-side credentials for off-chain auth
  • On-chain public key storage for account abstraction
  • Consider credential backup and recovery
  1. Cross-Chain Compatibility
  • Passkey public keys work across any EVM chain
  • Smart contract wallet addresses can be deterministic
  • Consider multi-chain deployment strategy
  1. User Experience
  • Provide clear onboarding flow
  • Support multiple passkeys per account
  • Implement graceful fallbacks
  1. Security Considerations
  • Validate all responses server-side
  • Implement proper challenge expiration
  • Consider rate limiting
  • Audit smart contracts thoroughly

Recommended Libraries

Library

Purpose

Platform

@simplewebauthn/browser

Frontend WebAuthn

JavaScript

@simplewebauthn/server

Backend verification

Node.js

webauthn4j

Backend verification

Java

py_webauthn

Backend verification

Python

go-webauthn

Backend verification

Go

passport-fido2

Express middleware

Node.js

WebAuthn Security Properties

Phishing Resistance

WebAuthn’s phishing resistance stems from origin binding:

  1. During registration, the credential is bound to the relying party ID (e.g., bank.com)
  2. During authentication, the browser automatically includes the actual origin in the signed data
  3. A phishing site at evil-bank.com cannot use credentials bound to bank.com
  4. The user doesn’t need to verify the URL – cryptography handles it

This makes WebAuthn a core component of phishing-resistant MFA strategies.

Replay Attack Prevention

Each authentication ceremony uses a unique challenge:

  • Server generates random challenge (minimum 16 bytes)
  • Authenticator signs the challenge with private key
  • Signature is valid only for that specific challenge
  • Captured signatures cannot be reused

Credential Isolation

WebAuthn credentials are isolated by design:

  • Unique key pair generated per relying party
  • No credential linkability between sites
  • Compromising one site reveals nothing about other credentials
  • Eliminates credential reuse vulnerabilities

Hardware-Backed Security

Private keys are protected by hardware:

  • TPM (Trusted Platform Module) on Windows
  • Secure Enclave on Apple devices
  • TEE (Trusted Execution Environment) on Android
  • Secure element on hardware security keys

Keys cannot be extracted even with full device access.

Browser and Platform Support

Current Support Status (2024)

Browser

Platform

WebAuthn Support

Passkey Sync

Chrome

Windows

Full

Via Google Account

Chrome

macOS

Full

Via Google Account

Chrome

Android

Full

Via Google Account

Safari

macOS

Full

Via iCloud Keychain

Safari

iOS

Full

Via iCloud Keychain

Firefox

Windows

Full

Limited

Firefox

macOS

Full

Limited

Edge

Windows

Full

Via Microsoft Account

Samsung Internet

Android

Full

Via Samsung Pass

Feature Detection

// Check if WebAuthn is available

function isWebAuthnAvailable() {

  return window.PublicKeyCredential !== undefined;

}

 

// Check if platform authenticator is available

async function isPlatformAuthenticatorAvailable() {

  if (!isWebAuthnAvailable()) return false;

  

  return await PublicKeyCredential

    .isUserVerifyingPlatformAuthenticatorAvailable();

}

 

// Check for conditional UI support (passkey autofill)

async function isConditionalUIAvailable() {

  if (!isWebAuthnAvailable()) return false;

  

  return await PublicKeyCredential

    .isConditionalMediationAvailable?.() ?? false;

}

 

// Usage

async function checkSupport() {

  const webauthn = isWebAuthnAvailable();

  const platform = await isPlatformAuthenticatorAvailable();

  const conditional = await isConditionalUIAvailable();

  

  console.log(`WebAuthn: ${webauthn}`);

  console.log(`Platform Authenticator: ${platform}`);

  console.log(`Conditional UI (Passkey Autofill): ${conditional}`);

}

 

Passkeys: WebAuthn Evolution

What Are Passkeys?

Passkeys are the consumer-friendly evolution of WebAuthn credentials. The term was introduced by Apple, Google, and Microsoft to make passwordless authentication more accessible.

Key characteristics:

  • Discoverable credentials (no username required to initiate)
  • Synced across devices via platform ecosystems
  • User-friendly terminology and UX
  • Based on existing WebAuthn/FIDO2 standards

Synced vs. Device-Bound Passkeys

Feature

Synced Passkeys

Device-Bound Passkeys

Storage

Cloud-synced

Single device only

Recovery

Automatic via cloud

Requires backup method

Security

High

Highest

Portability

Across ecosystem devices

Physical device required

Enterprise Control

Limited

Full

Best For

Consumer apps

High-security enterprise

Implementing Passkey Autofill (Conditional UI)

Modern browsers support passkey autofill, allowing users to select passkeys from the same UI as password autofill:

// Enable conditional mediation for passkey autofill

async function authenticateWithAutofill() {

  const options = await fetchAuthenticationOptions();

  

  try {

    const assertion = await navigator.credentials.get({

      publicKey: {

        …options,

        // Enable conditional UI

        mediation: ‘conditional’

      },

      // Also required for conditional UI

      mediation: ‘conditional’

    });

    

    return await verifyAssertion(assertion);

  } catch (error) {

    if (error.name === ‘NotAllowedError’) {

      // User cancelled or no credential available

      return null;

    }

    throw error;

  }

}

 

HTML for conditional UI:

<input 

  type=”text” 

  id=”username” 

  autocomplete=”username webauthn”

  placeholder=”Username or use passkey”

/>

Enterprise Deployment Considerations

Identity Provider Integration

Major identity providers support WebAuthn:

Microsoft Entra ID (Azure AD)

  • Native passkey/security key support
  • Windows Hello for Business integration
  • Conditional Access policies for FIDO2

Okta

  • WebAuthn authenticator enrollment
  • Passwordless sign-in flows
  • Admin controls for authenticator types

Google Workspace

  • Security key enforcement
  • Passkey support for Google accounts
  • Advanced Protection Program

Deployment Best Practices

  1. Start with Security-Conscious Users

    • IT and security teams
    • Executives with access to sensitive data
    • Users who have experienced phishing
  2. Require Multiple Authenticators

    • Primary platform authenticator (laptop biometric)
    • Backup security key
    • Consider allowing synced passkeys for recovery
  3. Implement Progressive Enforcement

    • Phase 1: Enable and encourage
    • Phase 2: Require for sensitive operations
    • Phase 3: Passwordless-only for eligible users
  4. Plan for Edge Cases

    • Shared workstations
    • Accessibility requirements
    • Legacy system access
    • Account recovery scenarios

Common Implementation Challenges

Challenge 1: Account Recovery

Problem: User loses all authenticators

Solutions:

  • Require multiple authenticators during registration
  • Implement secure recovery code generation
  • Use identity verification for recovery (with appropriate security)
  • Consider allowing synced passkeys as recovery option

Challenge 2: Cross-Device Authentication

Problem: User registers on laptop, needs to authenticate on phone

Solutions:

  • Enable hybrid/cross-device authentication (QR + Bluetooth)
  • Encourage platform authenticators that sync
  • Support multiple credential types

Challenge 3: Legacy Browser Support

Problem: Some users have outdated browsers

Solutions:

  • Implement graceful feature detection
  • Provide fallback to traditional authentication
  • Display upgrade recommendations
  • Consider proxy-based solutions for legacy apps

Challenge 4: Enterprise Device Management

Problem: Managing authenticators across thousands of devices

Solutions:

  • Integrate with MDM for platform authenticator management
  • Use attestation to enforce approved authenticators
  • Implement centralized credential lifecycle management
  • Leverage identity provider policies

Future of WebAuthn

Emerging Capabilities

Signal API (Proposed)

  • Allow relying parties to signal events to authenticators
  • Use case: Mark compromised credentials for deletion

Device Public Key Extension

  • Bind credentials to specific devices
  • Enhanced security for high-risk scenarios

Hybrid Transport Improvements

  • Better cross-device authentication UX
  • Reduced latency for QR/Bluetooth flows

Enterprise Attestation

  • More granular authenticator identification
  • Better policy enforcement capabilities

Industry Adoption Trends

  • Major platforms (Apple, Google, Microsoft) pushing passkeys aggressively
  • Financial services adopting for customer authentication
  • Healthcare organizations implementing for HIPAA compliance
  • Government agencies mandating phishing-resistant MFA

Conclusion

WebAuthn represents the most significant advancement in authentication security since the introduction of MFA. By replacing shared secrets with asymmetric cryptography and binding credentials to specific origins, WebAuthn eliminates entire categories of attacks that have plagued password-based authentication for decades.

Key Takeaways:

  • What is WebAuthn: A W3C standard enabling passwordless authentication using public-key cryptography
  • How does WebAuthn work: Registration creates unique key pairs; authentication proves possession without revealing secrets
  • How do I add WebAuthn passkeys to a dApp: Use the WebAuthn API with proper backend verification, or integrate with account abstraction for on-chain authentication
  • Security benefits: Phishing resistance, replay protection, credential isolation, hardware-backed keys
  • Passkeys: The consumer-friendly evolution of WebAuthn with cloud sync capabilities

Organizations implementing WebAuthn as part of their phishing-resistant MFA strategy gain protection against the credential-based attacks driving the majority of data breaches. Whether you’re securing a traditional web application or building the next generation of decentralized applications, WebAuthn provides the cryptographic foundation for truly secure authentication.

Ready to implement WebAuthn? TerraZone’s truePass platform provides enterprise-grade WebAuthn support with seamless integration, comprehensive management capabilities, and Zero Trust architecture. Contact us to learn how passwordless authentication can transform your security posture.

 

Welcome! Let's start the journey

AI Personal Consultant

Chat: AI Chat is not available - token for access to the API for text generation is not specified