@solidus-network/auth
Auth primitives for DID-based authentication. This package implements a challenge-response protocol where a verifier issues a nonce, the holder signs it as a verifiable presentation, and the verifier checks the signature.
npm install @solidus-network/authimport { createChallenge, verifyPresentation } from '@solidus-network/auth'createChallenge
Generates a cryptographic challenge for a DID holder to prove control of their private key.
createChallenge(did: string, ttlSeconds?: number): ChallengeParameters
| Name | Type | Default | Description |
|---|---|---|---|
did | string | — | The DID to challenge |
ttlSeconds | number | 300 | Time-to-live in seconds (5 minutes default) |
Returns a Challenge object.
interface Challenge {
id: string // UUID v4
did: string // The challenged DID
nonce: string // 32-byte random hex nonce
issuedAt: string // ISO 8601 timestamp
expiresAt: string // ISO 8601 timestamp (issuedAt + ttlSeconds)
}How it works:
- Generates a UUID v4 as the challenge ID
- Generates 32 bytes of cryptographically random data, hex-encoded as the nonce
- Sets
issuedAtto the current time - Sets
expiresAttoissuedAt + ttlSeconds(default 300 seconds)
Example
import { createChallenge } from '@solidus-network/auth'
// Default 5-minute TTL
const challenge = createChallenge('did:solidus:testnet:user123')
console.log(challenge)
// {
// id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
// did: "did:solidus:testnet:user123",
// nonce: "a4f2e8c1d3b5f6a7890123456789abcdef...",
// issuedAt: "2026-05-07T12:00:00.000Z",
// expiresAt: "2026-05-07T12:05:00.000Z"
// }
// Custom 60-second TTL
const shortChallenge = createChallenge(
'did:solidus:testnet:user123',
60
)verifyPresentation
Verifies a signed verifiable presentation against a challenge. Performs four sequential checks — if any check fails, verification stops and returns the failure reason.
verifyPresentation(
challenge: Challenge,
presentation: VerifiablePresentation,
getPublicKey: (did: string) => Promise<Uint8Array>
): Promise<PresentationVerificationResult>Parameters
| Name | Type | Description |
|---|---|---|
challenge | Challenge | The original challenge object |
presentation | VerifiablePresentation | The signed presentation from the holder |
getPublicKey | (did: string) => Promise<Uint8Array> | Callback to resolve a DID to its Ed25519 public key |
Returns a PresentationVerificationResult.
interface PresentationVerificationResult {
valid: boolean
did?: string
error?: string
}Verification Steps
The four checks run in order. If any fails, the remaining checks are skipped.
| Step | Check | Fails when |
|---|---|---|
| 1 | Expiry | Date.now() > challenge.expiresAt |
| 2 | Holder match | presentation.holder !== challenge.did |
| 3 | Nonce match | presentation.proof.challenge !== challenge.nonce |
| 4 | Signature | Ed25519 signature verification fails |
Presentation Format
The holder must construct a VerifiablePresentation in this format:
interface VerifiablePresentation {
'@context': ['https://www.w3.org/2018/credentials/v1']
type: ['VerifiablePresentation']
holder: string // The holder's DID
proof: {
type: 'Ed25519Signature2020'
created: string // ISO 8601 timestamp
verificationMethod: string // DID key reference (e.g. "did:solidus:...#key-1")
proofPurpose: 'authentication'
challenge: string // The nonce from the challenge
jws: string // Detached JWS signature
}
}JWS Format
The jws field uses detached JWS format: base64url(header)..base64url(signature) (note the two dots — the payload is omitted).
The header is:
{ "alg": "EdDSA", "b64": false, "crit": ["b64"] }The signature is computed over the challenge nonce using the holder’s Ed25519 private key.
Complete Auth Flow
Here is a full example showing challenge creation, presentation signing, and verification.
import { createChallenge, verifyPresentation } from '@solidus-network/auth'
import { ed25519 } from '@noble/ed25519'
// --- Verifier side ---
// 1. Create a challenge
const challenge = createChallenge('did:solidus:testnet:user123')
// Send challenge.id and challenge.nonce to the holder...
// --- Holder side ---
// 2. Build and sign a presentation
const presentation = {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiablePresentation'],
holder: 'did:solidus:testnet:user123',
proof: {
type: 'Ed25519Signature2020',
created: new Date().toISOString(),
verificationMethod: 'did:solidus:testnet:user123#key-1',
proofPurpose: 'authentication',
challenge: challenge.nonce,
jws: await signDetachedJws(challenge.nonce, privateKey),
},
}
// Send presentation back to verifier...
// --- Verifier side ---
// 3. Verify the presentation
const result = await verifyPresentation(
challenge,
presentation,
async (did) => {
// Resolve the DID to get its public key
const doc = await resolveDid(did)
return doc.verificationMethod[0].publicKeyBytes
}
)
if (result.valid) {
console.log('Authenticated:', result.did)
// Create session, issue JWT, etc.
} else {
console.log('Authentication failed:', result.error)
}Error Cases
| Error | Cause | Resolution |
|---|---|---|
"Challenge expired" | More than 300s (or custom TTL) elapsed | Issue a new challenge |
"Holder mismatch" | Presentation holder DID does not match challenged DID | Ensure the correct DID signs |
"Nonce mismatch" | Proof challenge field does not match issued nonce | Use the nonce from the original challenge |
"Invalid signature" | Ed25519 signature does not verify | Check key pair and signing logic |