This is a Node.js module to work with UCAN authorization tokens. It is a wrapper over the rs-ucan Rust library. To learn more about UCANs and how you might use them in your application, visit https://ucan.xyz or read the spec.
Current UCAN Specification is v0.10.0spec.
This module contains prebuilt platform-specific binaries for Node.js v18 on Windows x64 and Linux x64 (can be used in Docker containers without Rust setup). For other platforms it will build the binary from source.
To build from source, you must have the Rust toolchain installed. Check a supported version of Node and Rust. If you don't already have Rust installed, or don't have a supported version, go to the Rust web site for installation instructions.
npm i @myjoypin/node-ucan
# to rebuild
npm rebuild @myjoypin/node-ucan --foreground-scripts
UCANs can act the same as a classic JWT (Bearer) token, and also can grant a user the full control over their resources, with ability to delegate rights.
Example of delegation:
import { createDid, invokeUcan, verifyUcan } from "@myjoypin/node-ucan"
// secret part
// here are the server, Alice and Bob
const serverDidDocument = createDid()
const aliceDidDocument = createDid()
const bobDidDocument = createDid()
// public part
const serverDid = serverDidDocument.id
const aliceDid = aliceDidDocument.id
const bobDid = bobDidDocument.id
// Server provides some rights to Alice.
const aliceToken = await invokeUcan({
issuer: serverDidDocument.verificationMethod[0],
audience: aliceDid,
// 1 day
expiration: Math.ceil((Date.now() + 1000 * 60 * 60 * 24) / 1000),
capabilities: {
"api:app/xxx": {
"book/view": [{}],
"book/edit": [{}],
"user/is": [{ user_id: "111" }]
}
},
facts: {
app_id: "xxx",
user_id: "111"
}
})
// Alice can delegate some of her rights to Bob,
// without any interaction to the server.
const bobToken = await invokeUcan({
issuer: aliceDidDocument.verificationMethod[0],
audience: bobDid,
// 6 hours
expiration: Math.ceil((Date.now() + 1000 * 60 * 60 * 6) / 1000),
capabilities: {
"api:app/xxx": {
"book/view": [{}],
"user/is": [{ user_id: "111" }]
}
},
proofs: [aliceToken]
})
// Bob uses his token to acess some Alice's resource on the server.
const token = await invokeUcan({
issuer: bobDidDocument.verificationMethod[0],
audience: serverDid,
expiration: Math.ceil((Date.now() + 1000 * 60 * 60 * 6) / 1000),
capabilities: {
"api:app/xxx": {
"book/view": [{}],
"user/is": [{ user_id: "111" }]
}
},
proofs: [bobToken]
})
// Server verifies rights through the all proof chain.
const verification = await verifyUcan(token, {
rootIssuer: serverDid,
audience: serverDid,
requiredCapabilities: {
"api:app/{app_id}": {
"book/view": [{}],
"user/is": [{ user_id: "{user_id}" }]
}
},
requiredFacts: {
app_id: "*",
user_id: "*"
}
})
// Here the server can continue verification, checking token revocations by CID
// and performing custom caveat checks.
console.log("Access granted!")
console.log(JSON.stringify(verification, null, 2))
/*
{
"capabilities": {
"api:app/xxx": {
"book/view": [
{}
],
"user/is": [
{
"user_id": "111"
}
]
}
},
"facts": {
"app_id": "xxx",
"user_id": "111"
},
"cids": [
"bafkr4icmf3o2omytuqxkhzm76hd5ifdfnhhmharru622ca264rznwegdvy",
"bafkr4ieyr2wnbxe6ziuuougwngnfhnfeuw2e2anwcxyy36lsmcbh5zcguu",
"bafkr4ib24o6fsjpnmtzwvjrbptq244waiwwsdu67e27och6quzdd5azo4q"
]
}
*/
To be able to invoke an UCAN, you must have a DID (Decentralized Identifier).
Example of DID: "did:key:z6MkmftLP2owSZPneufrtWF7t9j2HL7sXYVgoroM59sjD7yx"
.
createDid(keyType?: String, useJoseFormat?: Boolean): DIDDocument
Only "did:key" DIDs are supported.
Supported key types:
- "Ed25519" | "Ed25519VerificationKey2018" | "JsonWebKey2020"
- "P256" | "P-256"
Additional types for DID, but you cannot sign UCANs with them:
- "X25519" | "X25519KeyAgreementKey2019"
- "Bls12381" | "Bls12381G2Key2020" | "BLS12381_G2"
- "Secp256k1" | "EcdsaSecp256k1VerificationKey2019" | "secp256k1"
Example:
import { createDid } from "@myjoypin/node-ucan"
const secretDidDocument = createDid()
console.log(JSON.stringify(secretDidDocument, null, 2))
"secretDidDocument" is a secret DID document (in JSON-LD format), containing your ID and private key. Please keep it in secret.
Optionally, you can specify a key type of the DID on creation (default is "Ed25519") and the flag to return document in JOSE format:
import { createDid } from "@myjoypin/node-ucan"
const secretDidDocument = createDid("P256", true)
console.log(JSON.stringify(secretDidDocument, null, 2))
A DID can be resolved to a DID document. The DID document (in JSON format) contains public information about the DID.
resolveDid(did: String, useJoseFormat?: Boolean): DIDDocument
Example:
import { resolveDid } from "@myjoypin/node-ucan"
const didDocument = resolveDid(
"did:key:z6MkmftLP2owSZPneufrtWF7t9j2HL7sXYVgoroM59sjD7yx"
)
console.log(JSON.stringify(didDocument, null, 2))
A DID can be restored from a private key (specified in "verificationMethod" field in the secret DID document).
restoreDid(privateKey: Object, useJoseFormat?: Boolean): DIDDocument
Example:
import { restoreDid } from "@myjoypin/node-ucan"
const privateKey = {
"id": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au#z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"type": "Ed25519VerificationKey2018",
"controller": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"publicKeyBase58": "8BJoSgFxJRuna8x5a952PfaYPAJgGpzQBwZMBZTcoPX",
"privateKeyBase58": "CjePJc2FYbQKTDaKeFyFRgKnzKF6DreCaX95b9x4z4Lu"
}
const secretDidDocument = restoreDid(privateKey)
console.log(JSON.stringify(secretDidDocument, null, 2))
The DID can be used to sign data and verify signatures.
simpleSign(privateKey: Object, message: String): String
simpleVerify(did: String, message: String, signature: String): Boolean
Example of signing a message:
import { simpleSign } from "@myjoypin/node-ucan"
const privateKey = {
"id": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au#z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"type": "Ed25519VerificationKey2018",
"controller": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"publicKeyBase58": "8BJoSgFxJRuna8x5a952PfaYPAJgGpzQBwZMBZTcoPX",
"privateKeyBase58": "CjePJc2FYbQKTDaKeFyFRgKnzKF6DreCaX95b9x4z4Lu"
}
const signature = simpleSign(privateKey, "message to be signed")
console.log(signature)
// 0f5591xbxJ7kx8WL4wNDAU5jWcodQ91kllAcm11HF69af8mJL4WNj858yphxUVmETD9L9F44paJu2r0eTs7dDA
Example of a signature verification:
import { simpleVerify } from "@myjoypin/node-ucan"
const isValid = simpleVerify(
"did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"message to be signed",
"0f5591xbxJ7kx8WL4wNDAU5jWcodQ91kllAcm11HF69af8mJL4WNj858yphxUVmETD9L9F44paJu2r0eTs7dDA")
console.log(isValid)
User-Controlled Authorization Network (UCAN) is a trustless, secure, local-first, user-originated authorization and revocation scheme.
We also call UCAN tokens UCANs.
invokeUcan({
// the private key
issuer: Object,
// audience DID
audience: String,
// Unix time in seconds when the token becomes expired.
expiration: number,
// Unix time in seconds after which token becomes enabled. Optional.
notBefore?: number,
// Capabilities object.
// Example:
// {
// "mailto:username@example.com": {
// "msg/receive": [{}],
// "msg/send": [{ draft: true }, { publish: true, topic: ["foo"] }]
// }
// }
capabilities: Capabilities,
// Facts object. Can contain any data linked with a token. Optional.
facts?: Object,
// Array of delegation proof tokens. Optional.
proofs?: Array<String>,
// Add a random nonce to this token. Optional, default false.
addNonce?: Boolean,
// Whenever to embed proof tokens to the "prf" field of facts. Doing so,
// UCAN is complete for self-verification. Optional, default true.
addProofFacts?: Boolean
}): Promise<String>
Example:
import { invokeUcan } from "@myjoypin/node-ucan"
const privateKey = {
"id": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au#z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"type": "Ed25519VerificationKey2018",
"controller": "did:key:z6MkeaSMPgvhHqvNu4yem96usVDaMxSA6A5M6CrVBTXUY2Au",
"publicKeyBase58": "8BJoSgFxJRuna8x5a952PfaYPAJgGpzQBwZMBZTcoPX",
"privateKeyBase58": "CjePJc2FYbQKTDaKeFyFRgKnzKF6DreCaX95b9x4z4Lu"
}
const token = await invokeUcan({
issuer: privateKey,
audience: "did:key:z6Mkmup4Wyv9kXKrmy1DB2bLhaviKSgPGSqjC1gCEtWoHjAY",
expiration: Math.ceil((Date.now() + 1000 * 60 * 60) / 1000),
capabilities: {
"mailto:username@example.com": {
"msg/receive": [{}],
"msg/send": [{ draft: true }, { publish: true, topic: ["foo"] }]
}
},
facts: {
a: "b"
}
})
console.log(token)
The DID which is the audience of a token can delegate own rights to other DID. Tranferred rights can be the same or lower level.
To be described...
decodeUcan(token: String): Ucan
Example:
import { decodeUcan } from "@myjoypin/node-ucan"
// const token = ...
let ucan = decodeUcan(token)
console.log(JSON.stringify(ucan, null, 2))
To verify a token, you should provide required capabilities in the following format:
{
"resource": {
"ability": [...caveat],
}
}
To be described...
verifyUcan(
// the token
token,
{
// The root rights issuer DID. An important field in the verification of
// rights delegation.
rootIssuer: String,
// Audience DID
audience: String,
// Required capabilities. You can use template variables from facts with
// "{var}" syntax. For example: "user/{user_id}".
// Example:
// {
// "mailto:username@example.com/x": {
// "msg/receive": [{}],
// "msg/send": [{ draft: true }, { publish: true, topic: ["foo"] }]
// }
// }
requiredCapabilities: Capabilities,
// Required facts. If some facts are required. Optional.
// To check for a field presence (with any value), use "*".
// Also, fact fields can be used for replacement in "requiredCapabilities",
// with "{var}" syntax. For example: "user/{user_id}".
// Example:
// {
// "user_id": "*"
// }
requiredFacts?: Object,
// If UCAN to be verified doesn't contain some proofs embedded, need to
// provide them. Optional.
knownTokens?: Array<String>
}
): Promise<{
// Capabilities allowed.
capabilities: Capabilities,
// Facts, if any.
facts?: Object,
// CIDs array. After successfull verification, use this list to check for
// revoked tokens.
cids: Array<String>,
}>
Example:
import { verifyUcan } from "@myjoypin/node-ucan"
// const rootIssuer = ...
// const audience = ...
// will throw if doesn't pass checks
await verifyUcan(token, {
rootIssuer,
audience,
requiredCapabilities: {
"mailto:username@example.com": {
"msg/receive": [{}],
"msg/send": [{ draft: true }, { publish: true, topic: ["foo"] }]
}
},
requiredFacts: {
a: "b"
}
})
console.log('Access granted')
-
Resource: ":"
The resource is an URL-like path to resource (it can be a real URL). "scheme" is any application specific scheme, like "api", "docs". "path" is an URL-like path, like "user/1", "user/1/post/2". The path includes access to all its sub-paths, for example "user/1" includes any of "user/1/post/1", "user/1/post/2", etc. Verification is performed by first by comparing schemes, then comparing each part of paths between "/" sequentally. The special path "*" means all in a capability, and "some" in the requirement resource, for example the requirement "user/ *" allows "user/1", compared to the requirement "user" which doesn't allow it (as in this context "user" means all users, but "user/ *" means some user, but not all users).
Examples:
Capability resource Required resource Includes user user/1 Yes user/1 user No (required is higher) user/1 user/1 Yes (are equal) user/1 user/1/doc/1 Yes (required is included) user/1 user/2 No (are not equal) user/1 doc/1 No (are not equal) * user/1 Yes (requred is included) user/1 * No (reqired is higher level) user/1 user/ * Yes user/ * user/1 Yes user/1/post/1 user/ * /post/2 No -
Ability: "/ability[/sub-ability]"
The ability is an action allowed for the resource. Its format is "namespace/ability[/sub-ability]". The special ability "*" always means "all" (in difference to its meaning for resource). It can be used at the end to include all sub-actions ("user/post/ *"), but not in the middle.
Examples:
Capability ability Required ability Enables user/post user/post Yes user/post user/post/draft Yes user/post/draft user/post No * user/post Yes user/post * No user/ * user/post Yes user/post user/ * No -
Caveats: "[{: }[, {}, ...]]"
To be described...
See the examples
folder.
This project is licensed under the MIT license.