@hellocoop/web-identity
v1.1.5
Published
Functions for generating and verifying JWT tokens used in the Verified Email Autocomplete protocol
Readme
@hellocoop/web-identity
⚠️ DEPRECATED - This package has been superseded by @hellocoop/email-verification.
Development Note: This package was collaboratively developed using spec-driven development with AI assistance. You can view the complete requirements, design, and implementation specifications that guided the development process.
TypeScript functions for generating and verifying JWT tokens used in the Verified Email Autocomplete protocol. This package provides complete implementations for RequestToken, IssuanceToken (SD-JWT), and PresentationToken (SD-JWT+KB) as defined in the web-identity specification.
🚀 Try It Live with Hellō
Want to see the Verified Email Autocomplete protocol in action? You can test it right now with hello.coop!
Quick Test Setup
Add DNS Record: Add this TXT record to your domain:
email._web-identity.yourdomain.com TXT "iss=hello.coop"Get User Hint Cookie: Visit Hellō Wallet, create a wallet if needed by logging in, and verify an email address at your domain. In your browser, inspect the page, select the application tab, and find the
user-hintcookie for wallet.hello.coop, and get its value.Run the Test: Use npx to test the complete flow:
npx @hellocoop/web-identity [email protected] user-hint <cookie-value>
Example
# After setting up DNS and getting your user-hint cookie
npx @hellocoop/web-identity [email protected] user-hint eyJhbGciOiJFZERTQSJ9...This will:
- ✅ Discover the issuer (hello.coop) via DNS
- ✅ Fetch hello.coop web-identity metadata and JWKS
- ✅ Generate a request token with a browser key pair
- ✅ Send the request to hello.coop issuance endpoint
- ✅ Verify and display the returned SD-JWT token
Perfect for testing your DNS setup and seeing the protocol in action! 🎯
Package Installation
npm install @hellocoop/web-identityQuick Start
ESM (ECMAScript Modules)
import {
// Most commonly used by RPs
verifyPresentationToken,
// Other token functions
generateRequestToken,
verifyRequestToken,
generateIssuanceToken,
verifyIssuanceToken,
generatePresentationToken,
// DNS discovery functions
discoverIssuer,
fetchWebIdentityMetadata,
fetchJWKS,
clearCaches,
// Types
type KeyResolver,
type WebIdentityMetadata,
type JWKSResponse,
type RequestOptions,
} from '@hellocoop/web-identity'CommonJS
const {
// Most commonly used by RPs
verifyPresentationToken,
// Other token functions
generateRequestToken,
verifyRequestToken,
generateIssuanceToken,
verifyIssuanceToken,
generatePresentationToken,
// Discovery functions
discoverIssuer,
fetchWebIdentityMetadata,
fetchJWKS,
clearCaches,
} = require('@hellocoop/web-identity')Note: The package provides dual ESM/CommonJS support with automatic module resolution based on your project configuration.
API Reference
PresentationToken Verification (Most Common for RPs)
verifyPresentationToken(token, expectedAudience, expectedNonce, keyResolver?)
🔥 Most commonly used function by Relying Parties - Verifies a PresentationToken (SD-JWT+KB) from browsers with automatic DNS-based key discovery.
Parameters:
token: string- SD-JWT+KB string to verifyexpectedAudience: string- Expected audience (RP's origin)expectedNonce: string- Expected nonce from RP's sessionkeyResolver?: KeyResolver- Optional function to resolve issuer's public key. If not provided, uses automatic DNS discovery
Returns: Promise<PresentationTokenPayload> - Object containing both SD-JWT and KB-JWT payloads
Example (Automatic DNS Discovery - Recommended):
// Simplest usage - automatic key discovery via DNS
const verified = await verifyPresentationToken(
presentationToken,
'https://rp.example',
'session-nonce-123',
)
console.log(verified.sdJwt.email) // '[email protected]'
console.log(verified.kbJwt.aud) // 'https://rp.example'Example (Custom Key Resolver):
// Custom key resolver for advanced use cases
const keyResolver = async (kid, issuer) => {
// Your custom logic to resolve keys
return await getPublicKeyFromSomewhere(kid, issuer)
}
const verified = await verifyPresentationToken(
presentationToken,
'https://rp.example',
'session-nonce-123',
keyResolver,
)RequestToken Functions
RequestTokens are used by browsers to request verified email tokens from issuers (step 3.4 & 4.1).
generateRequestToken(payload, jwk, options?)
Generates a RequestToken (JWT) with an embedded public key.
Parameters:
payload: RequestTokenPayload- Token payloadaud: string- Audience (issuer domain)nonce: string- Nonce provided by the RPemail: string- Email address to be verifiediat?: number- Optional issued at time (defaults to current time)
jwk: JWK- JSON Web Key containing private key, algorithm, and key IDoptions?: TokenGenerationOptions- Optional generation options
Returns: Promise<string> - Signed JWT string
Example:
const requestToken = await generateRequestToken(
{
aud: 'issuer.example',
nonce: '259c5eae-486d-4b0f-b666-2a5b5ce1c925',
email: '[email protected]',
},
browserPrivateKey,
)verifyRequestToken(token)
Verifies a RequestToken using the embedded public key.
Parameters:
token: string- JWT string to verify
Returns: Promise<RequestTokenPayload> - Verified payload
Example:
const verified = await verifyRequestToken(requestToken)
console.log(verified.email) // '[email protected]'Note: RequestTokens contain the public key embedded in the JWT header, so no external key resolver is needed.
IssuanceToken Functions
IssuanceTokens (SD-JWTs) are used by issuers to provide verified email tokens to browsers (step 4.2 & 5.1).
generateIssuanceToken(payload, jwk, options?)
Generates an IssuanceToken (SD-JWT) for verified email addresses.
Parameters:
payload: IssuanceTokenPayload- Token payloadiss: string- Issuer identifiercnf: { jwk: JWK }- Confirmation claim with browser's public keyemail: string- Verified email addressemail_verified: boolean- Must betrueiat?: number- Optional issued at time
jwk: JWK- Issuer's private keyoptions?: TokenGenerationOptions- Optional generation options
Returns: Promise<string> - Signed SD-JWT string
Example:
const issuanceToken = await generateIssuanceToken(
{
iss: 'issuer.example',
cnf: { jwk: browserPublicKey },
email: '[email protected]',
email_verified: true,
},
issuerPrivateKey,
)verifyIssuanceToken(token, keyResolver)
Verifies an IssuanceToken (SD-JWT) from issuers.
Parameters:
token: string- SD-JWT string to verifykeyResolver: KeyResolver- Function to resolve issuer's public key
Returns: Promise<IssuanceTokenPayload> - Verified payload
Example:
const keyResolver: KeyResolver = async (kid, issuer) => {
// Return the appropriate public key for verification
return await getIssuerPublicKey(kid, issuer)
}
const verified = await verifyIssuanceToken(issuanceToken, keyResolver)PresentationToken Functions
PresentationTokens (SD-JWT+KB) are used by browsers to present verified email tokens to relying parties (step 5.2 & 6.2-6.4).
generatePresentationToken(sdJwt, audience, nonce, jwk, options?)
Generates a PresentationToken (SD-JWT+KB) with key binding.
Parameters:
sdJwt: string- SD-JWT from issueraudience: string- RP's originnonce: string- Nonce from RP's sessionjwk: JWK- Browser's private keyoptions?: TokenGenerationOptions- Optional generation options
Returns: Promise<string> - SD-JWT+KB string (format: {SD-JWT}~{KB-JWT})
Example:
const presentationToken = await generatePresentationToken(
issuanceToken,
'https://rp.example',
'session-nonce-123',
browserPrivateKey,
)DNS Discovery Functions
The package provides DNS-based discovery functions to automatically find issuers and fetch their metadata according to the Verified Email Autocomplete specification.
discoverIssuer(emailOrDomain)
Discovers the web-identity issuer for an email address or domain via DNS TXT record lookup.
Parameters:
emailOrDomain: string- Email address (e.g.,[email protected]) or domain (e.g.,example.com)
Returns: Promise<string> - Issuer identifier (domain)
DNS Record Format: The function looks for TXT records at email._web-identity.$EMAIL_DOMAIN with format iss=issuer.example
Example:
// Discover issuer from email address
const issuer = await discoverIssuer('[email protected]')
console.log(issuer) // 'issuer.example'
// Or from domain directly
const issuer = await discoverIssuer('example.com')DNS Setup Example:
email._web-identity.example.com TXT iss=issuer.examplefetchWebIdentityMetadata(issuerIdentifier, options?)
Fetches web-identity metadata from an issuer domain's well-known endpoint.
Parameters:
issuerIdentifier: string- Issuer domain (e.g.,issuer.example)options?: RequestOptions- Optional request configurationtimeout?: number- Request timeout in milliseconds (default: 10000)cacheTimeout?: number- Cache timeout in milliseconds (default: 300000)
Returns: Promise<WebIdentityMetadata> - Metadata containing endpoints and supported algorithms
Example:
const metadata = await fetchWebIdentityMetadata('issuer.example', {
timeout: 5000, // 5 second timeout
cacheTimeout: 60000, // 1 minute cache
})
console.log(metadata.issuance_endpoint) // 'https://accounts.issuer.example/web-identity/issuance'
console.log(metadata.jwks_uri) // 'https://accounts.issuer.example/web-identity/jwks.json'
console.log(metadata.signing_alg_values_supported) // ['EdDSA', 'RS256']fetchJWKS(jwksUri, options?)
Fetches JWKS (JSON Web Key Set) from a JWKS URI.
Parameters:
jwksUri: string- JWKS URI from web-identity metadataoptions?: RequestOptions- Optional request configurationtimeout?: number- Request timeout in milliseconds (default: 10000)cacheTimeout?: number- Cache timeout in milliseconds (default: 300000)
Returns: Promise<JWKSResponse> - JWKS containing public keys
Example:
const jwks = await fetchJWKS(metadata.jwks_uri, {
timeout: 5000,
cacheTimeout: 300000, // 5 minute cache
})
console.log(jwks.keys.length) // Number of keys availableclearCaches()
Clears the in-memory caches for metadata and JWKS. Useful for testing or forcing fresh fetches.
Example:
// Clear all caches to force fresh fetches
clearCaches()Complete DNS Discovery Example
import {
discoverIssuer,
fetchWebIdentityMetadata,
fetchJWKS,
verifyIssuanceToken,
} from '@hellocoop/web-identity'
// 1. Discover issuer from email domain
const issuer = await discoverIssuer('[email protected]')
// 2. Fetch issuer metadata
const metadata = await fetchWebIdentityMetadata(issuer)
// 3. Fetch issuer's public keys
const jwks = await fetchJWKS(metadata.jwks_uri)
// 4. Create key resolver using fetched JWKS
const keyResolver = async (kid?: string, issuer?: string) => {
const key = jwks.keys.find((k) => k.kid === kid)
if (!key) {
throw new Error(`Key with ID '${kid}' not found`)
}
return key
}
// 5. Use with token verification
const verified = await verifyIssuanceToken(issuanceToken, keyResolver)Types
KeyResolver
type KeyResolver = (kid?: string, issuer?: string) => Promise<JWK | KeyLike>Function to resolve public keys for verification. Called with the key ID (kid) from the JWT header and optionally the issuer identifier.
Token Payloads
interface RequestTokenPayload {
aud: string
iat?: number
nonce: string
email: string
}
interface IssuanceTokenPayload {
iss: string
iat?: number
cnf: { jwk: JWK }
email: string
email_verified: boolean
}
interface PresentationTokenPayload {
sdJwt: IssuanceTokenPayload
kbJwt: {
aud: string
nonce: string
iat?: number
sd_hash?: string
}
}Generation Options
interface TokenGenerationOptions {
algorithm?: string // Override algorithm from JWK
expiresIn?: number // Token expiration (default: 60 seconds)
}DNS Discovery Types
interface WebIdentityMetadata {
issuance_endpoint: string
jwks_uri: string
signing_alg_values_supported?: string[]
}
interface JWKSResponse {
keys: JWK[]
}
interface RequestOptions {
timeout?: number // Request timeout in milliseconds (default: 10000)
cacheTimeout?: number // Cache timeout in milliseconds (default: 300000)
}Error Handling
The package provides specific error classes for different failure scenarios:
import {
WebIdentityError, // Base error class
MissingClaimError, // Required claim missing
InvalidSignatureError, // Signature verification failed
TimeValidationError, // Time-based validation failed
TokenFormatError, // Invalid token format
JWKValidationError, // JWK validation failed
EmailValidationError, // Email validation failed
DNSDiscoveryError, // DNS discovery failed
JWKSFetchError, // JWKS/metadata fetch failed
} from '@hellocoop/web-identity'
try {
const issuer = await discoverIssuer('[email protected]')
const metadata = await fetchWebIdentityMetadata(issuer)
const token = await generateRequestToken(payload, key)
} catch (error) {
if (error instanceof DNSDiscoveryError) {
console.log(`DNS discovery failed: ${error.message}`)
} else if (error instanceof JWKSFetchError) {
console.log(`JWKS fetch failed: ${error.message}`)
} else if (error instanceof MissingClaimError) {
console.log(`Missing claim: ${error.message}`)
} else if (error instanceof EmailValidationError) {
console.log(`Invalid email: ${error.message}`)
}
}Utility Functions
The package also exports utility functions that may be useful:
import {
// Cryptographic utilities
validateJWK, // Validate JWK structure
extractPublicKeyParameters, // Extract public key from JWK
calculateSHA256Hash, // Calculate SHA-256 hash
isValidEmail, // Validate email format
// Time utilities
getCurrentTimestamp, // Get current Unix timestamp
validateIatClaim, // Validate iat claim
TIME_VALIDATION_WINDOW, // Default time window (60 seconds)
// DNS discovery utilities
discoverIssuer, // Discover issuer from email/domain
fetchWebIdentityMetadata, // Fetch issuer metadata
fetchJWKS, // Fetch JWKS from URI
clearCaches, // Clear in-memory caches
} from '@hellocoop/web-identity'Module Compatibility
This package supports both ESM (ECMAScript Modules) and CommonJS environments:
- ✅ ESM:
importstatements (Node.js with"type": "module", modern bundlers) - ✅ CommonJS:
require()statements (traditional Node.js projects) - ✅ TypeScript: Full type definitions included for both module systems
- ✅ Bundlers: Works with Webpack, Rollup, Vite, and other modern bundlers
The package automatically provides the correct module format based on your project configuration.
Algorithm Support
The package supports the following cryptographic algorithms:
- RSA: RS256, RS384, RS512
- EdDSA: Ed25519 (recommended for new implementations)
- ECDSA: ES256, ES384, ES512
Security Features
- ✅ Automatic private key stripping - Private key material is automatically removed from
cnfclaims - ✅ Time-based validation - All tokens validated within 60-second windows
- ✅ Email format validation - Comprehensive email validation with security checks
- ✅ JWK validation - Thorough validation of key parameters
- ✅ Cross-algorithm support - Mix and match RSA and EdDSA keys
- ✅ Independent verification - Separate test suite validates token generation
Security Considerations
- Libraries do not confirm issuer domain is eTLD+1. Browsers will enforce in actual deployments.
Performance
Typical performance benchmarks on modern hardware:
- RequestToken generation: ~1.3ms average
- RequestToken verification: ~0.15ms average
- EdDSA operations: ~0.3ms round-trip
- Memory usage: <7MB for 1000 operations
Complete Example
ESM (ECMAScript Modules)
import {
generateRequestToken,
verifyRequestToken,
generateIssuanceToken,
verifyIssuanceToken,
generatePresentationToken,
verifyPresentationToken,
type KeyResolver,
} from '@hellocoop/web-identity'
// 1. Browser generates RequestToken
const requestToken = await generateRequestToken(
{
aud: 'issuer.example',
nonce: 'rp-nonce-123',
email: '[email protected]',
},
browserPrivateKey,
)
// 2. Issuer verifies RequestToken
const requestPayload = await verifyRequestToken(requestToken)
// 3. Issuer generates IssuanceToken
const issuanceToken = await generateIssuanceToken(
{
iss: 'issuer.example',
cnf: { jwk: extractedBrowserPublicKey },
email: requestPayload.email,
email_verified: true,
},
issuerPrivateKey,
)
// 4. Browser verifies IssuanceToken
const keyResolver: KeyResolver = async (kid, issuer) => {
return await getIssuerPublicKey(kid, issuer)
}
const issuancePayload = await verifyIssuanceToken(issuanceToken, keyResolver)
// 5. Browser generates PresentationToken
const presentationToken = await generatePresentationToken(
issuanceToken,
'https://rp.example',
'rp-nonce-123',
browserPrivateKey,
)
// 6. Relying Party verifies PresentationToken (automatic DNS discovery)
const presentationPayload = await verifyPresentationToken(
presentationToken,
'https://rp.example',
'rp-nonce-123',
)
console.log('Verified email:', presentationPayload.sdJwt.email)CommonJS
const {
generateRequestToken,
verifyRequestToken,
generateIssuanceToken,
verifyIssuanceToken,
generatePresentationToken,
verifyPresentationToken,
} = require('@hellocoop/web-identity')
// Key resolver function
const keyResolver = async (kid, issuer) => {
return await getIssuerPublicKey(kid, issuer)
}
// Same usage as ESM example above...
// (The rest of the implementation is identical)Specification
This package implements the Verified Email Autocomplete protocol. For detailed protocol information, please refer to the specification.
Contributing
This package is part of the Hello Identity Co-op packages monorepo. Issues and contributions are welcome.
