Key Server OAuth
The KeyServerClient enables dApps to verify user ownership of GitHub and Discord accounts through the Key Server OAuth flow. Each successful verification produces a DAHR (Demos Attestation Hash Response) attestation - a cryptographic proof that the Key Server performed the verification.
Installation
The KeyServerClient is included in the SDK:
npm install @kynesyslabs/demosdk
# or
bun add @kynesyslabs/demosdk
Quick Start
import { KeyServerClient } from "@kynesyslabs/demosdk/keyserver";
const client = new KeyServerClient({
endpoint: "http://localhost:3030", // Node RPC endpoint
nodePubKey: "abc123...", // Node's Ed25519 public key
});
// Complete OAuth verification
const result = await client.verifyOAuth("github", {
onAuthUrl: (url) => window.open(url),
});
console.log("Verified user:", result.user.username);
console.log("Provider ID:", result.user.providerId);
API Reference
KeyServerClient
Constructor
const client = new KeyServerClient({
endpoint: string; // Key Server endpoint URL
nodePubKey: string; // Node's Ed25519 public key (hex)
defaultWalletAddress?: string; // Optional default wallet for binding
});
getProviders()
Get list of available OAuth providers.
const providers = await client.getProviders();
// Returns: ["github", "discord"]
initiateOAuth()
Start an OAuth flow and get the authorization URL.
const result = await client.initiateOAuth("github", {
scopes?: string[]; // Optional custom scopes
timeout?: number; // Flow timeout in ms (default: 600000)
});
// Returns:
// {
// success: true,
// authUrl: "https://github.com/login/oauth/authorize?...",
// state: "abc123",
// expiresAt: 1703001234567
// }
pollOAuth()
Check the status of an OAuth flow.
const result = await client.pollOAuth(state, walletBinding?);
// Parameters:
// - state: string - State identifier from initiateOAuth
// - walletBinding?: WalletBinding - Optional wallet binding with signature
// Returns:
// {
// success: true,
// status: "pending" | "completed" | "failed" | "expired",
// result?: OAuthUserInfo, // Present when completed
// attestation?: DAHRAttestation, // Present when completed
// error?: { code, message } // Present when failed
// }
verifyOAuth()
Complete OAuth flow with automatic polling (recommended).
const result = await client.verifyOAuth("github", {
onAuthUrl: (url, state) => {
// Display URL to user (e.g., open popup, show QR code)
window.open(url);
},
onPoll: (attempt, status) => {
// Optional: Show polling progress
console.log(`Attempt ${attempt}: ${status}`);
},
timeout: 300000, // 5 minutes (default: 600000)
pollInterval: 2000, // 2 seconds (default: 2000)
scopes: ["user:email"], // Optional custom scopes
walletBinding: async (state) => {
// Optional: Bind a wallet address to this verification
const message = `demos-oauth-bind:${state}`;
const signature = await wallet.signMessage(message);
return { address: wallet.address, signature, signatureType: "evm" };
},
});
// Returns:
// {
// success: true,
// user: OAuthUserInfo,
// attestation: DAHRAttestation
// }
Types
OAuthUserInfo
User identity information from OAuth provider.
interface OAuthUserInfo {
service: "github" | "discord";
providerId: string; // Provider's user ID
username: string; // Provider's username
email?: string; // If email scope was requested
avatarUrl?: string; // User avatar URL
verifiedAt: number; // Unix timestamp
}
WalletBinding
Wallet binding for proving ownership during OAuth verification.
interface WalletBinding {
address: string; // Wallet address to bind
signature: string; // Signature proving ownership
signatureType: "evm" | "solana" | "ed25519"; // Signature scheme
}
The message to sign is: demos-oauth-bind:{state} where state is from initiateOAuth().
DAHRAttestation
Cryptographic proof of verification.
interface DAHRAttestation {
requestHash: string; // SHA256 of request payload
responseHash: string; // SHA256 of response payload
signature: {
type: "Ed25519";
data: string; // Hex-encoded signature
};
metadata: {
sessionId: string;
timestamp: number;
keyServerPubKey: string;
nodePubKey: string;
version: string;
walletBinding?: WalletBinding; // Present if wallet was bound
};
}
Attestation Verification
You can cryptographically verify that the Key Server actually signed the attestation:
import {
KeyServerClient,
verifyOAuthAttestation
} from "@kynesyslabs/demosdk/keyserver";
const result = await client.verifyOAuth("github", { ... });
// Verify the attestation
const verification = verifyOAuthAttestation(
result,
KNOWN_KEY_SERVER_PUBKEY, // Expected Key Server public key
{
maxAge: 3600000, // Optional: max attestation age (default: 1 hour)
expectedNodePubKey: "...", // Optional: validate specific node
}
);
if (!verification.valid) {
console.error("Invalid attestation:", verification.reason);
}
Verification Options
| Option | Type | Default | Description |
|---|
maxAge | number | 3600000 | Max attestation age in ms. Set to 0 to disable. |
expectedNodePubKey | string | - | Validate the attestation came from a specific node |
Error Handling
The client throws OAuthError for all OAuth-related errors:
import { KeyServerClient, OAuthError } from "@kynesyslabs/demosdk/keyserver";
try {
const result = await client.verifyOAuth("github", { ... });
} catch (error) {
if (error instanceof OAuthError) {
switch (error.code) {
case "OAUTH_DENIED":
console.log("User denied access");
break;
case "OAUTH_EXPIRED":
console.log("OAuth flow expired");
break;
case "OAUTH_TIMEOUT":
console.log("Polling timed out");
break;
case "NETWORK_ERROR":
console.log("Network error:", error.message);
break;
default:
console.log(`Error [${error.code}]: ${error.message}`);
}
}
}
Error Codes
| Code | Description |
|---|
OAUTH_INVALID_SERVICE | Invalid OAuth provider specified |
OAUTH_NOT_CONFIGURED | OAuth not configured on server |
OAUTH_NOT_AVAILABLE | OAuth service unavailable |
OAUTH_DENIED | User denied authorization |
OAUTH_EXPIRED | OAuth flow expired |
OAUTH_PROVIDER_ERROR | Error from OAuth provider |
OAUTH_STATE_NOT_FOUND | Invalid or expired state |
OAUTH_STATE_MISMATCH | State parameter mismatch |
OAUTH_TIMEOUT | Polling timed out |
NETWORK_ERROR | Network connection error |
INTERNAL_ERROR | Internal server error |
Wallet Binding
Wallet binding allows you to cryptographically prove ownership of a blockchain wallet during OAuth verification. The wallet address and signature are included in the DAHR attestation, creating a verifiable link between the OAuth identity and the wallet.
Why Use Wallet Binding?
- Sybil Resistance: Link social accounts to wallets to prevent one person from claiming multiple identities
- Access Control: Gate access based on both wallet and verified social identity
- Attestation Integrity: The wallet binding is part of the signed attestation, tamper-proof
How It Works
- User initiates OAuth flow
- dApp receives the
state parameter in onAuthUrl callback
- User signs message
demos-oauth-bind:{state} with their wallet
- Signature is sent with subsequent poll requests
- Key Server verifies the signature and includes binding in attestation
Usage
You can provide wallet binding as a function that receives the state:
const result = await client.verifyOAuth("github", {
onAuthUrl: (url) => window.open(url),
walletBinding: async (state) => {
// Sign with user's connected wallet
const message = `demos-oauth-bind:${state}`;
const signature = await wallet.signMessage(message);
return {
address: wallet.address,
signature,
signatureType: "evm" // or "solana", "ed25519"
};
},
});
// The attestation now includes the wallet binding
console.log("Bound wallet:", result.attestation.metadata.walletBinding?.address);
Or provide a pre-computed binding (if state is known ahead of time):
const result = await client.verifyOAuth("github", {
onAuthUrl: (url) => window.open(url),
walletBinding: {
address: "0x123...",
signature: "0xabc...",
signatureType: "evm"
},
});
Supported Signature Types
| Type | Description | Message Format |
|---|
evm | Ethereum/EVM personal_sign or eth_sign | UTF-8 string |
solana | Solana signMessage | UTF-8 bytes |
ed25519 | Raw Ed25519 signature | UTF-8 bytes |
Full Example
Complete example with UI feedback and wallet binding:
import {
KeyServerClient,
OAuthError,
verifyOAuthAttestation
} from "@kynesyslabs/demosdk/keyserver";
const KEY_SERVER_PUBKEY = "your_key_server_public_key_hex";
async function verifyGitHubAccount(wallet: { address: string; signMessage: (msg: string) => Promise<string> }) {
const client = new KeyServerClient({
endpoint: "http://localhost:3030",
nodePubKey: "your_node_public_key_hex",
});
try {
// Check available providers
const providers = await client.getProviders();
console.log("Available providers:", providers);
// Start verification with wallet binding
const result = await client.verifyOAuth("github", {
onAuthUrl: (url, state) => {
// Open GitHub auth in popup
const popup = window.open(url, "github-oauth", "width=600,height=700");
// Store state for CSRF protection
sessionStorage.setItem("oauth_state", state);
},
onPoll: (attempt, status) => {
// Update UI with progress
document.getElementById("status").textContent =
`Waiting for authorization... (${status})`;
},
timeout: 300000, // 5 minutes
// Bind wallet to this OAuth verification
walletBinding: async (state) => {
const message = `demos-oauth-bind:${state}`;
const signature = await wallet.signMessage(message);
return {
address: wallet.address,
signature,
signatureType: "evm"
};
},
});
// Verify attestation cryptographically
const verification = verifyOAuthAttestation(result, KEY_SERVER_PUBKEY);
if (!verification.valid) {
throw new Error(`Attestation invalid: ${verification.reason}`);
}
// Success!
console.log("Verified user:", result.user.username);
console.log("Provider ID:", result.user.providerId);
console.log("Attestation valid:", verification.valid);
// Check wallet binding in attestation
if (result.attestation.metadata.walletBinding) {
console.log("Bound wallet:", result.attestation.metadata.walletBinding.address);
}
return result;
} catch (error) {
if (error instanceof OAuthError) {
if (error.code === "OAUTH_DENIED") {
alert("You denied access. Please try again.");
} else if (error.code === "OAUTH_TIMEOUT") {
alert("Verification timed out. Please try again.");
} else {
alert(`Verification failed: ${error.message}`);
}
}
throw error;
}
}
Architecture
The Key Server OAuth flow works as follows:
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐
│ dApp │────▶│ SDK │────▶│ Node RPC │────▶│ Key Server │
│ │ │ (Client) │ │ │ │ │
└─────────┘ └──────────┘ └────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ GitHub / │
│ Discord │
└──────────────┘
- dApp calls
KeyServerClient.verifyOAuth()
- SDK sends request to Node RPC
- Node proxies to Key Server (same host)
- Key Server redirects user to GitHub/Discord
- User authorizes, Key Server receives callback
- Key Server creates DAHR attestation
- Attestation returned through the chain to dApp
The Key Server and Node communicate on the same host, so no signatures are needed for this internal communication. The DAHR attestation provides cryptographic proof to external parties.